Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7de428f
Let us start from here
ssloy Aug 30, 2025
1d65cbf
Bresenham #1
ssloy Feb 22, 2025
49111e5
Bresenham #2a
ssloy Feb 22, 2025
ee28757
Bresenham #2b
ssloy Feb 22, 2025
13562c1
Bresenham #3
ssloy Feb 22, 2025
debf68d
Bresenham #4: first measurement
ssloy Feb 23, 2025
3cf5900
Bresenham 4b: second measurement
ssloy Feb 23, 2025
16b1850
Bresenham 4c: third measurement
ssloy Feb 23, 2025
15bb729
Bresenham 4d: fourth measurement
ssloy Feb 23, 2025
05c0a0a
integer Bresenham
ssloy Feb 23, 2025
fb3cf74
Point cloud rendering
ssloy Feb 23, 2025
fc4218c
Wireframe rendering
ssloy Feb 23, 2025
74c5d26
Triangle rasterization, the starting point
ssloy Feb 25, 2025
bcddf63
Scanline #1
ssloy Feb 25, 2025
241fc6f
Scanline #2
ssloy Feb 25, 2025
13ea279
Scanline #3
ssloy Feb 25, 2025
29ccb94
Scanline #4
ssloy Feb 25, 2025
467012c
Bounding box rasterization #1
ssloy Feb 25, 2025
52a7ace
Bounding box rasterization #2
ssloy Feb 25, 2025
d31cf74
Model rendering, no back-face culling
ssloy Feb 25, 2025
a409be2
Model rendering with back-face culling
ssloy Feb 25, 2025
418dc34
Painter's algorithm
ssloy Feb 25, 2025
c676d07
depth interpolation
ssloy Apr 10, 2025
baba9c8
z-buffer hidden faces removal
ssloy Apr 10, 2025
385c26f
vectors/matrices
ssloy Apr 15, 2025
ba101cf
rotate the camera
ssloy Apr 22, 2025
9a6176d
central projection
ssloy Apr 22, 2025
b6261fd
float point z-buffer
ssloy Apr 22, 2025
48bf07c
camera handling
ssloy Apr 29, 2025
36c5fb3
better camera handling
ssloy May 28, 2025
c31ad88
refactoring time (still random colors)
ssloy Aug 12, 2025
a5c4e5d
ambient light
ssloy Aug 12, 2025
24c7d90
diffuse light
ssloy Aug 12, 2025
6afd8a6
specular highlight
ssloy Aug 12, 2025
c3719a6
normal interpolation
ssloy Aug 14, 2025
773f2d1
global space normal mapping
ssloy Aug 14, 2025
91e9c71
diffuse+specular mapping
ssloy Aug 27, 2025
850c71b
tangent space normal mapping
ssloy Aug 27, 2025
d59a98c
linear interpolation perspective correction
ssloy Aug 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ endif()

find_package(OpenMP COMPONENTS CXX)

set(SOURCES main.cpp model.cpp our_gl.cpp tgaimage.cpp)
set(SOURCES main.cpp our_gl.cpp model.cpp tgaimage.cpp)

add_executable(${PROJECT_NAME} ${SOURCES})
target_link_libraries(${PROJECT_NAME} PRIVATE $<$<BOOL:${OpenMP_CXX_FOUND}>:OpenMP::OpenMP_CXX>)
Expand Down
108 changes: 53 additions & 55 deletions main.cpp
Original file line number Diff line number Diff line change
@@ -1,81 +1,79 @@
#include <limits>
#include "model.h"
#include "our_gl.h"
#include "model.h"

extern mat<4,4> ModelView; // "OpenGL" state matrices
extern mat<4,4> Projection;
extern mat<4,4> ModelView, Perspective; // "OpenGL" state matrices and
extern std::vector<double> zbuffer; // the depth buffer

struct Shader : IShader {
struct PhongShader : IShader {
const Model &model;
vec3 uniform_l; // light direction in view coordinates
mat<3,2> varying_uv; // triangle uv coordinates, written by the vertex shader, read by the fragment shader
mat<3,3> varying_nrm; // normal per vertex to be interpolated by FS
mat<3,3> view_tri; // triangle in view coordinates
vec4 l; // light direction in eye coordinates
vec2 varying_uv[3]; // triangle uv coordinates, written by the vertex shader, read by the fragment shader
vec4 varying_nrm[3]; // normal per vertex to be interpolated by the fragment shader
vec4 tri[3]; // triangle in view coordinates

Shader(const vec3 l, const Model &m) : model(m) {
uniform_l = normalized((ModelView*vec4{l.x, l.y, l.z, 0.}).xyz()); // transform the light vector to view coordinates
PhongShader(const vec3 light, const Model &m) : model(m) {
l = normalized((ModelView*vec4{light.x, light.y, light.z, 0.})); // transform the light vector to view coordinates
}

virtual void vertex(const int iface, const int nthvert, vec4& gl_Position) {
vec3 n = model.normal(iface, nthvert);
vec3 v = model.vert(iface, nthvert);
gl_Position = ModelView * vec4{v.x, v.y, v.z, 1.};
varying_uv[nthvert] = model.uv(iface, nthvert);
varying_nrm[nthvert] = (ModelView.invert_transpose() * vec4{n.x, n.y, n.z, 0.}).xyz();
view_tri[nthvert] = gl_Position.xyz();
gl_Position = Projection * gl_Position;
virtual vec4 vertex(const int face, const int vert) {
varying_uv[vert] = model.uv(face, vert);
varying_nrm[vert] = ModelView.invert_transpose() * model.normal(face, vert);
vec4 gl_Position = ModelView * model.vert(face, vert);
tri[vert] = gl_Position;
return Perspective * gl_Position; // in clip coordinates
}

virtual bool fragment(const vec3 bar, TGAColor &gl_FragColor) const {
vec3 bn = normalized(bar * varying_nrm); // per-vertex normal interpolation
vec2 uv = bar * varying_uv; // tex coord interpolation

mat<3,3> AI = mat<3,3>{ {view_tri[1] - view_tri[0], view_tri[2] - view_tri[0], bn} }.invert(); // for the math refer to the tangent space normal mapping lecture
vec3 i = AI * vec3{varying_uv[1].x - varying_uv[0].x, varying_uv[2].x - varying_uv[0].x, 0}; // https://github.com/ssloy/tinyrenderer/wiki/Lesson-6bis-tangent-space-normal-mapping
vec3 j = AI * vec3{varying_uv[1].y - varying_uv[0].y, varying_uv[2].y - varying_uv[0].y, 0};
mat<3,3> B = mat<3,3>{ { normalized(i), normalized(j), bn } }.transpose();

vec3 n = normalized(B * model.normal(uv)); // transform the normal from the texture to the tangent space
vec3 r = normalized(n * (n * uniform_l)*2 - uniform_l); // reflected light direction, specular mapping is described here: https://github.com/ssloy/tinyrenderer/wiki/Lesson-6-Shaders-for-the-software-renderer
double diff = std::max(0., n * uniform_l); // diffuse light intensity
double spec = std::pow(std::max(-r.z, 0.), 5+sample2D(model.specular(), uv)[0]); // specular intensity, note that the camera lies on the z-axis (in view), therefore simple -r.z

TGAColor c = sample2D(model.diffuse(), uv);
for (int i : {0,1,2})
gl_FragColor[i] = std::min<int>(10 + c[i]*(diff + spec), 255); // (a bit of ambient light, diff + spec), clamp the result
return false; // do not discard the pixel
virtual std::pair<bool,TGAColor> fragment(const vec3 bar) const {
mat<2,4> E = { tri[1]-tri[0], tri[2]-tri[0] };
mat<2,2> U = { varying_uv[1]-varying_uv[0], varying_uv[2]-varying_uv[0] };
mat<2,4> T = U.invert() * E;
mat<4,4> D = {normalized(T[0]), // tangent vector
normalized(T[1]), // bitangent vector
normalized(varying_nrm[0]*bar[0] + varying_nrm[1]*bar[1] + varying_nrm[2]*bar[2]), // interpolated normal
{0,0,0,1}}; // Darboux frame
vec2 uv = varying_uv[0] * bar[0] + varying_uv[1] * bar[1] + varying_uv[2] * bar[2];
vec4 n = normalized(D.transpose() * model.normal(uv));
vec4 r = normalized(n * (n * l)*2 - l); // reflected light direction
double ambient = .4; // ambient light intensity
double diffuse = 1.*std::max(0., n * l); // diffuse light intensity
double specular = (.5+2.*sample2D(model.specular(), uv)[0]/255.) * std::pow(std::max(r.z, 0.), 35); // specular intensity, note that the camera lies on the z-axis (in eye coordinates), therefore simple r.z, since (0,0,1)*(r.x, r.y, r.z) = r.z
TGAColor gl_FragColor = sample2D(model.diffuse(), uv);
for (int channel : {0,1,2})
gl_FragColor[channel] = std::min<int>(255, gl_FragColor[channel]*(ambient + diffuse + specular));
return {false, gl_FragColor}; // do not discard the pixel
}
};

int main(int argc, char** argv) {
if (2>argc) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " obj/model.obj" << std::endl;
return 1;
}

constexpr int width = 800; // output image size
constexpr int height = 800;
constexpr vec3 light_dir{1,1,1}; // light source
constexpr vec3 eye{1,1,3}; // camera position
constexpr vec3 center{0,0,0}; // camera direction
constexpr vec3 up{0,1,0}; // camera up vector
constexpr vec3 light{ 1, 1, 1}; // light source
constexpr vec3 eye{-1, 0, 2}; // camera position
constexpr vec3 center{ 0, 0, 0}; // camera direction
constexpr vec3 up{ 0, 1, 0}; // camera up vector

lookat(eye, center, up); // build the ModelView matrix
viewport(width/8, height/8, width*3/4, height*3/4); // build the Viewport matrix
projection(norm(eye-center)); // build the Projection matrix
std::vector<double> zbuffer(width*height, std::numeric_limits<double>::max());
lookat(eye, center, up); // build the ModelView matrix
init_perspective(norm(eye-center)); // build the Perspective matrix
init_viewport(width/16, height/16, width*7/8, height*7/8); // build the Viewport matrix
init_zbuffer(width, height);
TGAImage framebuffer(width, height, TGAImage::RGB, {177, 195, 209, 255});

TGAImage framebuffer(width, height, TGAImage::RGB); // the output image
for (int m=1; m<argc; m++) { // iterate through all input objects
Model model(argv[m]);
Shader shader(light_dir, model);
for (int t=0; t<model.nfaces(); t++) { // for every triangle
vec4 clip_vert[3]; // triangle coordinates (clip coordinates), written by VS, read by FS
for (int v : {0,1,2})
shader.vertex(t, v, clip_vert[v]); // call the vertex shader for each triangle vertex
rasterize(clip_vert, shader, framebuffer, zbuffer); // actual rasterization routine call
for (int m=1; m<argc; m++) { // iterate through all input objects
Model model(argv[m]); // load the data
PhongShader shader(light, model);
for (int f=0; f<model.nfaces(); f++) { // iterate through all facets
Triangle clip = { shader.vertex(f, 0), // assemble the primitive
shader.vertex(f, 1),
shader.vertex(f, 2) };
rasterize(clip, shader, framebuffer); // rasterize the primitive
}
}

framebuffer.write_tga_file("framebuffer.tga");
return 0;
}
Expand Down
32 changes: 17 additions & 15 deletions model.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <fstream>
#include <sstream>
#include "model.h"

Expand All @@ -12,20 +13,20 @@ Model::Model(const std::string filename) {
char trash;
if (!line.compare(0, 2, "v ")) {
iss >> trash;
vec3 v;
vec4 v = {0,0,0,1};
for (int i : {0,1,2}) iss >> v[i];
verts.push_back(v);
} else if (!line.compare(0, 3, "vn ")) {
iss >> trash >> trash;
vec3 n;
vec4 n;
for (int i : {0,1,2}) iss >> n[i];
norms.push_back(normalized(n));
} else if (!line.compare(0, 3, "vt ")) {
iss >> trash >> trash;
vec2 uv;
for (int i : {0,1}) iss >> uv[i];
tex.push_back({uv.x, 1-uv.y});
} else if (!line.compare(0, 2, "f ")) {
} else if (!line.compare(0, 2, "f ")) {
int f,t,n, cnt = 0;
iss >> trash;
while (iss >> f >> trash >> t >> trash >> n) {
Expand All @@ -40,41 +41,42 @@ Model::Model(const std::string filename) {
}
}
}
std::cerr << "# v# " << nverts() << " f# " << nfaces() << " vt# " << tex.size() << " vn# " << norms.size() << std::endl;
std::cerr << "# v# " << nverts() << " f# " << nfaces() << std::endl;
auto load_texture = [&filename](const std::string suffix, TGAImage &img) {
size_t dot = filename.find_last_of(".");
if (dot==std::string::npos) return;
std::string texfile = filename.substr(0,dot) + suffix;
std::cerr << "texture file " << texfile << " loading " << (img.read_tga_file(texfile.c_str()) ? "ok" : "failed") << std::endl;
};
load_texture("_diffuse.tga", diffusemap );
load_texture("_nm_tangent.tga", normalmap );
load_texture("_nm_tangent.tga", normalmap);
load_texture("_spec.tga", specularmap);
}

const TGAImage& Model::diffuse() const { return diffusemap; }
const TGAImage& Model::specular() const { return specularmap; }
int Model::nverts() const { return verts.size(); }
int Model::nfaces() const { return facet_vrt.size()/3; }

vec3 Model::vert(const int i) const {
vec4 Model::vert(const int i) const {
return verts[i];
}

vec3 Model::vert(const int iface, const int nthvert) const {
vec4 Model::vert(const int iface, const int nthvert) const {
return verts[facet_vrt[iface*3+nthvert]];
}

vec3 Model::normal(const vec2 &uvf) const {
TGAColor c = normalmap.get(uvf[0]*normalmap.width(), uvf[1]*normalmap.height());
return vec3{(double)c[2],(double)c[1],(double)c[0]}*2./255. - vec3{1,1,1};
vec4 Model::normal(const int iface, const int nthvert) const {
return norms[facet_nrm[iface*3+nthvert]];
}

vec4 Model::normal(const vec2 &uv) const {
TGAColor c = normalmap.get(uv[0]*normalmap.width(), uv[1]*normalmap.height());
return normalized(vec4{(double)c[2],(double)c[1],(double)c[0],0}*2./255. - vec4{1,1,1,0});
}

vec2 Model::uv(const int iface, const int nthvert) const {
return tex[facet_tex[iface*3+nthvert]];
}

vec3 Model::normal(const int iface, const int nthvert) const {
return norms[facet_nrm[iface*3+nthvert]];
}
const TGAImage& Model::diffuse() const { return diffusemap; }
const TGAImage& Model::specular() const { return specularmap; }

13 changes: 7 additions & 6 deletions model.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
#include "tgaimage.h"

class Model {
std::vector<vec3> verts = {}; // array of vertices ┐ generally speaking, these arrays
std::vector<vec3> norms = {}; // array of normal vectors │ do not have the same size
std::vector<vec4> verts = {}; // array of vertices ┐ generally speaking, these arrays
std::vector<vec4> norms = {}; // array of normal vectors │ do not have the same size
std::vector<vec2> tex = {}; // array of tex coords ┘ check the logs of the Model() constructor
std::vector<int> facet_vrt = {}; // ┐ per-triangle indices in the above arrays,
std::vector<int> facet_nrm = {}; // │ the size is supposed to be
Expand All @@ -15,12 +15,13 @@ class Model {
Model(const std::string filename);
int nverts() const; // number of vertices
int nfaces() const; // number of triangles
vec3 vert(const int i) const; // 0 <= i < nverts()
vec3 vert(const int iface, const int nthvert) const; // 0 <= iface <= nfaces(), 0 <= nthvert < 3
vec3 normal(const int iface, const int nthvert) const; // normal coming from the "vn x y z" entries in the .obj file
vec3 normal(const vec2 &uv) const; // normal vector from the normal map texture
vec4 vert(const int i) const; // 0 <= i < nverts()
vec4 vert(const int iface, const int nthvert) const; // 0 <= iface <= nfaces(), 0 <= nthvert < 3
vec4 normal(const int iface, const int nthvert) const; // normal coming from the "vn x y z" entries in the .obj file
vec4 normal(const vec2 &uv) const; // normal vector from the normal map texture
vec2 uv(const int iface, const int nthvert) const; // uv coordinates of triangle corners
const TGAImage& diffuse() const;
const TGAImage& specular() const;

};

68 changes: 34 additions & 34 deletions our_gl.cpp
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
#include <algorithm>
#include "our_gl.h"

mat<4,4> ModelView;
mat<4,4> Viewport;
mat<4,4> Projection;
mat<4,4> ModelView, Viewport, Perspective; // "OpenGL" state matrices
std::vector<double> zbuffer; // depth buffer

void viewport(const int x, const int y, const int w, const int h) {
Viewport = {{{w/2., 0, 0, x+w/2.}, {0, h/2., 0, y+h/2.}, {0,0,1,0}, {0,0,0,1}}};
void lookat(const vec3 eye, const vec3 center, const vec3 up) {
vec3 n = normalized(eye-center);
vec3 l = normalized(cross(up,n));
vec3 m = normalized(cross(n, l));
ModelView = mat<4,4>{{{l.x,l.y,l.z,0}, {m.x,m.y,m.z,0}, {n.x,n.y,n.z,0}, {0,0,0,1}}} *
mat<4,4>{{{1,0,0,-center.x}, {0,1,0,-center.y}, {0,0,1,-center.z}, {0,0,0,1}}};
}

void projection(const double f) { // check https://en.wikipedia.org/wiki/Camera_matrix
Projection = {{{1,0,0,0}, {0,-1,0,0}, {0,0,1,0}, {0,0,-1/f,0}}};
void init_perspective(const double f) {
Perspective = {{{1,0,0,0}, {0,1,0,0}, {0,0,1,0}, {0,0, -1/f,1}}};
}

void lookat(const vec3 eye, const vec3 center, const vec3 up) { // check https://github.com/ssloy/tinyrenderer/wiki/Lesson-5-Moving-the-camera
vec3 z = normalized(center-eye);
vec3 x = normalized(cross(up,z));
vec3 y = normalized(cross(z, x));
ModelView = mat<4,4>{{{x.x,x.y,x.z,0}, {y.x,y.y,y.z,0}, {z.x,z.y,z.z,0}, {0,0,0,1}}} *
mat<4,4>{{{1,0,0,-eye.x}, {0,1,0,-eye.y}, {0,0,1,-eye.z}, {0,0,0,1}}};
void init_viewport(const int x, const int y, const int w, const int h) {
Viewport = {{{w/2., 0, 0, x+w/2.}, {0, h/2., 0, y+h/2.}, {0,0,1,0}, {0,0,0,1}}};
}

vec3 barycentric(const vec2 tri[3], const vec2 P) {
mat<3,3> ABC = {{ {tri[0].x, tri[0].y, 1.}, {tri[1].x, tri[1].y, 1.}, {tri[2].x, tri[2].y, 1.} }};
if (ABC.det()<1) return {-1,1,1}; // for a degenerate triangle generate negative coordinates, it will be thrown away by the rasterizator
return ABC.invert_transpose() * vec3{P.x, P.y, 1.};
void init_zbuffer(const int width, const int height) {
zbuffer = std::vector(width*height, -1000.);
}

void rasterize(const vec4 clip_verts[3], const IShader &shader, TGAImage &image, std::vector<double> &zbuffer) {
vec4 pts [3] = { Viewport*clip_verts[0], Viewport*clip_verts[1], Viewport*clip_verts[2] }; // screen coordinates before persp. division
vec2 pts2[3] = { (pts[0]/pts[0].w).xy(), (pts[1]/pts[1].w).xy(), (pts[2]/pts[2].w).xy() }; // screen coordinates after perps. division
void rasterize(const Triangle &clip, const IShader &shader, TGAImage &framebuffer) {
vec4 ndc[3] = { clip[0]/clip[0].w, clip[1]/clip[1].w, clip[2]/clip[2].w }; // normalized device coordinates
vec2 screen[3] = { (Viewport*ndc[0]).xy(), (Viewport*ndc[1]).xy(), (Viewport*ndc[2]).xy() }; // screen coordinates

mat<3,3> ABC = {{ {screen[0].x, screen[0].y, 1.}, {screen[1].x, screen[1].y, 1.}, {screen[2].x, screen[2].y, 1.} }};
if (ABC.det()<1) return; // backface culling + discarding triangles that cover less than a pixel

int bbminx = std::max(0, static_cast<int>(std::min(std::min(pts2[0].x, pts2[1].x), pts2[2].x))); // bounding box for the triangle
int bbminy = std::max(0, static_cast<int>(std::min(std::min(pts2[0].y, pts2[1].y), pts2[2].y))); // clipped by the screen
int bbmaxx = std::min(image.width() -1, static_cast<int>(std::max(std::max(pts2[0].x, pts2[1].x), pts2[2].x)));
int bbmaxy = std::min(image.height()-1, static_cast<int>(std::max(std::max(pts2[0].y, pts2[1].y), pts2[2].y)));
auto [bbminx,bbmaxx] = std::minmax({screen[0].x, screen[1].x, screen[2].x}); // bounding box for the triangle
auto [bbminy,bbmaxy] = std::minmax({screen[0].y, screen[1].y, screen[2].y}); // defined by its top left and bottom right corners
#pragma omp parallel for
for (int x=bbminx; x<=bbmaxx; x++) { // rasterize the bounding box
for (int y=bbminy; y<=bbmaxy; y++) {
vec3 bc_screen = barycentric(pts2, {static_cast<double>(x), static_cast<double>(y)});
vec3 bc_clip = { bc_screen.x/pts[0].w, bc_screen.y/pts[1].w, bc_screen.z/pts[2].w }; // check https://github.com/ssloy/tinyrenderer/wiki/Technical-difficulties-linear-interpolation-with-perspective-deformations
for (int x=std::max<int>(bbminx, 0); x<=std::min<int>(bbmaxx, framebuffer.width()-1); x++) { // clip the bounding box by the screen
for (int y=std::max<int>(bbminy, 0); y<=std::min<int>(bbmaxy, framebuffer.height()-1); y++) {
vec3 bc_screen = ABC.invert_transpose() * vec3{static_cast<double>(x), static_cast<double>(y), 1.}; // barycentric coordinates of {x,y} w.r.t the triangle
vec3 bc_clip = { bc_screen.x/clip[0].w, bc_screen.y/clip[1].w, bc_screen.z/clip[2].w }; // check https://github.com/ssloy/tinyrenderer/wiki/Technical-difficulties-linear-interpolation-with-perspective-deformations
bc_clip = bc_clip / (bc_clip.x + bc_clip.y + bc_clip.z);
double frag_depth = bc_clip * vec3{ clip_verts[0].z, clip_verts[1].z, clip_verts[2].z };
if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0 || frag_depth > zbuffer[x+y*image.width()]) continue;
TGAColor color;
if (shader.fragment(bc_clip, color)) continue; // fragment shader can discard current fragment
zbuffer[x+y*image.width()] = frag_depth;
image.set(x, y, color);
if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue; // negative barycentric coordinate => the pixel is outside the triangle
double z = bc_screen * vec3{ ndc[0].z, ndc[1].z, ndc[2].z }; // linear interpolation of the depth
if (z <= zbuffer[x+y*framebuffer.width()]) continue; // discard fragments that are too deep w.r.t the z-buffer
auto [discard, color] = shader.fragment(bc_clip);
if (discard) continue; // fragment shader can discard current fragment
zbuffer[x+y*framebuffer.width()] = z; // update the z-buffer
framebuffer.set(x, y, color); // update the framebuffer
}
}
}
Expand Down
Loading