diff --git a/CMakeLists.txt b/CMakeLists.txt index c3c3372a..b2681857 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 $<$:OpenMP::OpenMP_CXX>) diff --git a/main.cpp b/main.cpp index eda62883..b3994fb8 100644 --- a/main.cpp +++ b/main.cpp @@ -1,81 +1,79 @@ -#include -#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 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(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 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(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 zbuffer(width*height, std::numeric_limits::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 #include #include "model.h" @@ -12,12 +13,12 @@ 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 ")) { @@ -25,7 +26,7 @@ Model::Model(const std::string filename) { 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) { @@ -40,7 +41,7 @@ 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; @@ -48,33 +49,34 @@ Model::Model(const std::string filename) { 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; } diff --git a/model.h b/model.h index ce5786cc..0f7fb575 100644 --- a/model.h +++ b/model.h @@ -2,8 +2,8 @@ #include "tgaimage.h" class Model { - std::vector verts = {}; // array of vertices ┐ generally speaking, these arrays - std::vector norms = {}; // array of normal vectors │ do not have the same size + std::vector verts = {}; // array of vertices ┐ generally speaking, these arrays + std::vector norms = {}; // array of normal vectors │ do not have the same size std::vector tex = {}; // array of tex coords ┘ check the logs of the Model() constructor std::vector facet_vrt = {}; // ┐ per-triangle indices in the above arrays, std::vector facet_nrm = {}; // │ the size is supposed to be @@ -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; + }; diff --git a/our_gl.cpp b/our_gl.cpp index 0e0c4284..7217015a 100644 --- a/our_gl.cpp +++ b/our_gl.cpp @@ -1,51 +1,51 @@ +#include #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 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 &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(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(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(std::max(std::max(pts2[0].x, pts2[1].x), pts2[2].x))); - int bbmaxy = std::min(image.height()-1, static_cast(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(x), static_cast(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(bbminx, 0); x<=std::min(bbmaxx, framebuffer.width()-1); x++) { // clip the bounding box by the screen + for (int y=std::max(bbminy, 0); y<=std::min(bbmaxy, framebuffer.height()-1); y++) { + vec3 bc_screen = ABC.invert_transpose() * vec3{static_cast(x), static_cast(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 } } } diff --git a/our_gl.h b/our_gl.h index 44433433..d0090f01 100644 --- a/our_gl.h +++ b/our_gl.h @@ -1,16 +1,18 @@ #include "tgaimage.h" #include "geometry.h" -void viewport(const int x, const int y, const int w, const int h); -void projection(const double coeff=0); // coeff = -1/c void lookat(const vec3 eye, const vec3 center, const vec3 up); +void init_perspective(const double f); +void init_viewport(const int x, const int y, const int w, const int h); +void init_zbuffer(const int width, const int height); struct IShader { static TGAColor sample2D(const TGAImage &img, const vec2 &uvf) { return img.get(uvf[0] * img.width(), uvf[1] * img.height()); } - virtual bool fragment(const vec3 bar, TGAColor &color) const = 0; + virtual std::pair fragment(const vec3 bar) const = 0; }; -void rasterize(const vec4 clip_verts[3], const IShader &shader, TGAImage &image, std::vector &zbuffer); +typedef vec4 Triangle[3]; // a triangle primitive is made of three ordered points +void rasterize(const Triangle &clip, const IShader &shader, TGAImage &framebuffer); diff --git a/tgaimage.cpp b/tgaimage.cpp index e23883cf..801c6f37 100644 --- a/tgaimage.cpp +++ b/tgaimage.cpp @@ -2,7 +2,11 @@ #include #include "tgaimage.h" -TGAImage::TGAImage(const int w, const int h, const int bpp) : w(w), h(h), bpp(bpp), data(w*h*bpp, 0) {} +TGAImage::TGAImage(const int w, const int h, const int bpp, TGAColor c) : w(w), h(h), bpp(bpp), data(w*h*bpp, 0) { + for (int j=0; j