Graphics Programming Virtual Meetup


Discord

Tiny Renderer
Software Rasterizer
Lessons 0-2
Following SSloy's Tutorial
My reference implementation
Lesson 0
Setup
Project Setup
- Needs`tgaimage.h` header
- Then copy-paste code
#include "tgaimage.h"
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
int main(int argc, char** argv)
{
TGAImage image(100, 100, TGAImage::RGB);
image.set(52, 41, red);
// i want to have the origin at the left bottom corner of the image
image.flip_vertically();
image.write_tga_file("output.tga");
return 0;
}Output (scaled up)

Lesson 1
Bresenham’s Line Drawing Algorithm
First Attempt
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color)
{
// step size of 0.1
for (float t = 0.; t < 1.; t += .01)
{
// linearly interpolate between x0 & x1 and y0 & y1
int x = x0 + (x1 - x0) * t;
int y = y0 + (y1 - y0) * t;
image.set(x, y, color);
}
}Simply iterate "along" the line segment, turning on pixels as you go over them
Issues
- Only takes 100 steps
- Leaves gaps!
- No aliasing
line(-100, -100, 500, 500, image, white);
Second Attempt
void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color)
{
//increment X by one every step
for (int x = x0; x <= x1; x++)
{
// get the current distance we are from x0 to x1 on a range of 0 to 1
float t = (x - x0) / (float)(x1 - x0);
// linearly interpolate y using that value between 0 to 1
int y = y0 * (1. - t) + y1 * t;
image.set(x, y, color);
}
}Have a step size of 1 pixel on the x axis, then linearly interpolate for the y axis
Better?
- Doesn't account for y axis!
- If the line goes down faster than it goes over , the y axis needs to take many steps but the x only takes one.
- We can do better!

line(-100, -100, 500, 500, image, white);
line(40, 40, 60, 260, image, white);
line(40, 40, 260, 60, image, white);Attempt 3
if (std::abs(x0-x1)<std::abs(y0-y1)) {
std::swap(x0, y0);
std::swap(x1, y1);
steep = true;
}
}Solution: Choose the steeper axis and iterate on that
Find the larger delta and make "X" represent that
Account for 'negative direction'
if (x0 > x1) { // make it left−to−right
std::swap(x0, x1);
std::swap(y0, y1);
}Now "X" is just which ever was the steeper axis
for (int x = x0; x <= x1; x++) {
float t = (x - x0) / (float)(x1 - x0);
int y = y0 * (1. - t) + y1 * t;
if (steep) {
image.set(y, x, color); // if transposed, de−transpose
} else {
image.set(x, y, color);
}
}Much better!

Attempt-4
- Factor out repeated calculations
- `derrer` is the slope
- Used to update the non major axis
- `error` is an accumulator
int dx = x1 - x0;
int dy = y1 - y0;
float derror = std::abs(dy / float(dx));
float error = 0;
int y = y0;- Modify for loop
- Remove linear interpolation calculation every iteration
- Update off-axis value if `error` > 0.5
- and reset accumulator
for (int x = x0; x <= x1; x++) {
if (steep) {
image.set(y, x, color);
} else {
image.set(x, y, color);
}
error += derror;
if (error > .5) {
//make sure we increment in the 'right' direction
y += (y1 > y0 ? 1 : -1);
error -= 1.;
}
}
Fixed Diagonal issues!
Attempt 5
- Remove floating point entirely
- Do this by changing the scale of `error` and `derror`
- Use minor axis difference instead of raw 'slope'
int dx = x1 - x0;
int dy = y1 - y0;
int derror2 = std::abs(dy) * 2;
int error2 = 0;
int y = y0;
for (int x = x0; x <= x1; x++) {
if (steep) {
image.set(y, x, color);
} else {
image.set(x, y, color);
}
error2 += derror2;
if (error2 > dx) {
y += (y1 > y0 ? 1 : -1);
error2 -= dx * 2;
}
}Wireframe Rendering
Must load the `.obj` models first
Copy-Paste `model.h` `model.cpp` and `geometry.h` into the project
Model model{"obj/african_head.obj"};
TGAImage image(width, height, TGAImage::RGB);
for (int i = 0; i < model.nfaces(); i++) {
std::vector<int> face = model.face(i);
for (int j = 0; j < 3; j++) {
Vec3f v0 = model.vert(face[j]);
Vec3f v1 = model.vert(face[(j + 1) % 3]);
int x0 = (v0.x + 1.) * width / 2.;
int y0 = (v0.y + 1.) * height / 2.;
int x1 = (v1.x + 1.) * width / 2.;
int y1 = (v1.y + 1.) * height / 2.;
line(x0, y0, x1, y1, image, white);
}
}

Lesson 2
Triangle Rasterization & back face culling
Line Sweep Algorithm
- Sort vertices of the triangle by their y-coordinates
- Rasterize simultaneously the left and the right sides of the triangle
- Draw a horizontal line segment between the left and the right boundary points
implementation
void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
// sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!)
if (t0.y>t1.y) std::swap(t0, t1);
if (t0.y>t2.y) std::swap(t0, t2);
if (t1.y>t2.y) std::swap(t1, t2);
line(t0, t1, image, green);
line(t1, t2, image, green);
line(t2, t0, image, red);
}
Issue
- There are 'two' green lines for each red
- Can't easily sweep left to right
- Solution: Split red into two lines, create 2 subtriangles
- Second triangle is rendered here

void triangle(Vec2i t0, Vec2i t1, Vec2i t2, TGAImage &image, TGAColor color) {
// sort the vertices, t0, t1, t2 lower−to−upper (bubblesort yay!)
if (t0.y>t1.y) std::swap(t0, t1);
if (t0.y>t2.y) std::swap(t0, t2);
if (t1.y>t2.y) std::swap(t1, t2);
int total_height = t2.y-t0.y;
for (int y=t0.y; y<=t1.y; y++) {
int segment_height = t1.y-t0.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t0.y)/segment_height;
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t0 + (t1-t0)*beta;
if (A.x>B.x) std::swap(A, B);
for (int j=A.x; j<=B.x; j++) {
image.set(j, y, color); // attention, due to int casts t0.y+i != A.y
}
}
for (int y=t1.y; y<=t2.y; y++) {
int segment_height = t2.y-t1.y+1;
float alpha = (float)(y-t0.y)/total_height;
float beta = (float)(y-t1.y)/segment_height;
Vec2i A = t0 + (t2-t0)*alpha;
Vec2i B = t1 + (t2-t1)*beta;
if (A.x>B.x) std::swap(A, B);
for (int j=A.x; j<=B.x; j++) {
image.set(j, y, color); // attention, due to int casts t0.y+i != A.y
}
}
}
Final Output
Is there a better way?
- Line-sweep works for monolithic processors
- Modern hardware isn't a single core
- Determine if a pixel is within a triangle
- Then shade it accordingly
Possible psuedo code
triangle(vec2 points[3]) {
vec2 bbox[2] = find_bounding_box(points);
for (each pixel in the bounding box) {
if (inside(points, pixel)) {
put_pixel(pixel);
}
}
}Barycentric Coordinates
- Given a triangle ABC and a point P
- The coordinates of P with respect to ABC
Point P has coordinates (u,v) in the basis A, AB, and AC
A barycentric coordinate system (in 2D) is when the location of a point is specified by reference to a triangle's vertices.

Rearrange
Linear System of Equations
Rewrite in matrix form
- Want a vector (u,v,1) which is orthogonal to both (ABx,ACx,PAx) and (ABy,ACy,PAy)
- To find an intersection of two straight lines in a plane, it is sufficient to compute the cross product.
Vec3f barycentric(Vec2i *pts, Vec2i P)
{
Vec3f u = cross(
Vec3f(pts[2][0] - pts[0][0], pts[1][0] - pts[0][0], pts[0][0] - P[0]),
Vec3f(pts[2][1] - pts[0][1], pts[1][1] - pts[0][1], pts[0][1] - P[1]));
/* `pts` and `P` has integer value as coordinates
so `abs(u[2])` < 1 means `u[2]` is 0, that means triangle is degenerate
in this case return something with negative coordinates */
if (std::abs(u[2]) < 1) return Vec3f(-1, 1, 1);
return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
}void triangle(Vec2i *pts, TGAImage &image, TGAColor color) {
Vec2i bboxmin(image.get_width()-1, image.get_height()-1);
Vec2i bboxmax(0, 0);
Vec2i clamp(image.get_width()-1, image.get_height()-1);
for (int i=0; i<3; i++) {
for (int j=0; j<2; j++) {
bboxmin[j] = std::max(0, std::min(bboxmin[j], pts[i][j]));
bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
}
}
Vec2i P;
for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) {
for (P.y=bboxmin.y; P.y<=bboxmax.y; P.y++) {
Vec3f bc_screen = barycentric(pts, P);
// if P is > (0,0,0), then P is withing the triangle
if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue;
image.set(P.x, P.y, color);
}
}
} Plug into psuedocode

Tada!
Lets Draw a model instead
We'll use random colors too
for (int i = 0; i < model.nfaces(); i++) {
std::vector<int> face = model.face(i);
vec2 screen_coords[3];
for (int j = 0; j < 3; j++) {
vec3 world_coords = model.vert(face[j]);
screen_coords[j] =
vec2((world_coords.x + 1.) * width / 2.,
(world_coords.y + 1.) * height / 2.);
}
triangle(screen_coords, image,
TGAColor(rand() % 255, rand() % 255, rand() % 255, 255));
}
Now to add shading & lighting
Simple lighting: Lambertian Reflectance
Or just Lamberts cosine law

for (int i=0; i<model->nfaces(); i++) {
std::vector<int> face = model->face(i);
Vec2i screen_coords[3];
Vec3f world_coords[3];
for (int j=0; j<3; j++) {
Vec3f v = model->vert(face[j]);
screen_coords[j] = Vec2i((v.x+1.)*width/2., (v.y+1.)*height/2.);
world_coords[j] = v;
}
Vec3f n = (world_coords[2]-world_coords[0])^(world_coords[1]-world_coords[0]);
n.normalize();
float intensity = n*light_dir;
// Negative direction means lighting is behind the polygon
// Allows very simple version of backface culling
if (intensity>0) {
triangle(screen_coords, image,
TGAColor(intensity*255, intensity*255, intensity*255, 255));
}
}
Graphics Programming Virtual Meetup
TinyRenderer Lesson 0-2
By Charles Giessen
TinyRenderer Lesson 0-2
- 144