diff --git a/.gitignore b/.gitignore index 4698a99e..06353271 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -build/ framebuffer.tga diff --git a/.gitpod.yml b/.gitpod.yml index beac9686..c6b757a8 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -2,11 +2,8 @@ image: file: Dockerfile tasks: - command: > - mkdir --parents build && - cd build && - cmake .. && - make && - ./tinyrenderer ../obj/diablo3_pose/diablo3_pose.obj ../obj/floor.obj && + cmake -Bbuild && + cmake --build build --parallel && + build/tinyrenderer obj/diablo3_pose/diablo3_pose.obj obj/floor.obj && convert framebuffer.tga framebuffer.png && - open framebuffer.png && - cd .. + open framebuffer.png diff --git a/CMakeLists.txt b/CMakeLists.txt index c2a0b94e..b2681857 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,29 @@ -cmake_minimum_required (VERSION 2.8) -project(tinyrenderer) +cmake_minimum_required(VERSION 3.12...3.26) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(NOT is_multi_config AND NOT (CMAKE_BUILD_TYPE OR DEFINED ENV{CMAKE_BUILD_TYPE})) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Release default") +endif() + +project(tinyrenderer LANGUAGES CXX) -find_package(OpenMP) -if(OPENMP_FOUND) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${OpenMP_EXE_LINKER_FLAGS}") +set(CMAKE_CXX_STANDARD 20) + +option(iwyu "Run include-what-you-use") +if(iwyu) + find_program(IWYU_EXE NAMES include-what-you-use REQUIRED) + set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE ${IWYU_EXE}) endif() -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU|Intel") + add_compile_options(-Wall) endif() -file(GLOB SOURCES *.h *.cpp) +find_package(OpenMP COMPONENTS CXX) + +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>) +file(GENERATE OUTPUT .gitignore CONTENT "*") diff --git a/README.md b/README.md index 1394ed2d..23cdb815 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,101 @@ -# Tiny Renderer or how OpenGL works: software rendering in 500 lines of code +# Software rendering in 500 lines of bare C++ + +The code itself is of little interest. Check the course notes: +1. [Introduction](https://haqr.eu/tinyrenderer/) +2. [Bresenham’s line drawing algorithm](https://haqr.eu/tinyrenderer/bresenham/) +3. [Triangle rasterization](https://haqr.eu/tinyrenderer/rasterization/) +4. [Primer on barycentric coordinates](https://haqr.eu/tinyrenderer/barycentric/) +5. [Hidden faces removal](https://haqr.eu/tinyrenderer/z-buffer/) +6. [A crude (but simple) approach to camera handling](https://haqr.eu/tinyrenderer/camera-naive/) +7. [Better camera handling](https://haqr.eu/tinyrenderer/camera/) +8. [Shading](https://haqr.eu/tinyrenderer/shading/) +9. [More data!](https://haqr.eu/tinyrenderer/textures/) +10. [Tangent space normal mapping](https://haqr.eu/tinyrenderer/tangent/) +11. [Shadow mapping](https://haqr.eu/tinyrenderer/shadow/) +12. [Indirect lighting](https://haqr.eu/tinyrenderer/ssao/) +13. [Bonus: toon shading](https://haqr.eu/tinyrenderer/toon/) +14. [Afterword](https://haqr.eu/tinyrenderer/afterword/) + +In this series of articles, I aim to demonstrate how OpenGL, Vulkan, Metal, and DirectX work by writing a simplified clone from scratch. +Surprisingly, many people struggle with the initial hurdle of learning a 3D graphics API. +To help with this, I have prepared a short series of lectures, after which my students are able to produce quite capable renderers. + +The task is as follows: using no third-party libraries (especially graphics-related ones), we will generate an image like this: + +![](https://haqr.eu/tinyrenderer/home/africanhead.png) + +_Warning: This is a training material that loosely follows the structure of modern 3D graphics libraries. +It is a **software renderer**. +**I do not intend to show how to write GPU applications — I want to show how they work.** +I firmly believe that understanding this is essential for writing efficient applications using 3D libraries._ + +## The starting point + +The final code consists of about 500 lines. +My students typically require 10 to 20 hours of programming to start producing such renderers. +The input is a 3D model composed of a triangulated mesh and textures. +The output is a rendereding. +There is no graphical interface, the program simply generates an image. + +To minimize external dependencies, I provide my students with a single class for handling [TGA](http://en.wikipedia.org/wiki/Truevision_TGA) files — +one of the simplest formats supporting RGB, RGBA, and grayscale images. +This serves as our foundation for image manipulation. +At the beginning, the only available functionality (besides loading and saving images) is the ability to set the color of a single pixel. + +There are no built-in functions for drawing line segments or triangles — we will implement all of this manually. +While I provide my own source code, written alongside my students, I do not recommend using it directly, as doing the work yourself is essential to understanding the concepts. +The complete code is available on [github](https://github.com/ssloy/tinyrenderer), and you can find the initial source code I provide to my students [here](https://github.com/ssloy/tinyrenderer/tree/706b2dfecff65daeb93de568ee2c2bd87f277860). +Behold, here is the starting point: + +```cpp +#include "tgaimage.h" -# Check [the wiki](https://github.com/ssloy/tinyrenderer/wiki) for the detailed lessons. +constexpr TGAColor white = {255, 255, 255, 255}; // attention, BGRA order +constexpr TGAColor green = { 0, 255, 0, 255}; +constexpr TGAColor red = { 0, 0, 255, 255}; +constexpr TGAColor blue = {255, 128, 64, 255}; +constexpr TGAColor yellow = { 0, 200, 255, 255}; -## compilation -```sh -git clone https://github.com/ssloy/tinyrenderer.git && -cd tinyrenderer && -mkdir build && -cd build && -cmake .. && -cmake --build . -j && -./tinyrenderer ../obj/diablo3_pose/diablo3_pose.obj ../obj/floor.obj -``` -The rendered image is saved to `framebuffer.tga`. +int main(int argc, char** argv) { + constexpr int width = 64; + constexpr int height = 64; + TGAImage framebuffer(width, height, TGAImage::RGB); -You can open the project in Gitpod, a free online dev evironment for GitHub: -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/ssloy/tinyrenderer) + int ax = 7, ay = 3; + int bx = 12, by = 37; + int cx = 62, cy = 53; -On open, the editor will compile & run the program as well as open the resulting image in the editor's preview. -Just change the code in the editor and rerun the script (use the terminal's history) to see updated images. + framebuffer.set(ax, ay, white); + framebuffer.set(bx, by, white); + framebuffer.set(cx, cy, white); -## The main idea + framebuffer.write_tga_file("framebuffer.tga"); + return 0; +} +``` -**My source code is irrelevant. Read the wiki and implement your own renderer. Only when you suffer through all the tiny details you will learn what is going on.** +It produces the 64x64 image `framebuffer.tga`, here I scaled it for better readability: -In [this series of articles](https://github.com/ssloy/tinyrenderer/wiki), I want to show the way OpenGL works by writing its clone (a much simplified one). Surprisingly enough, I often meet people who cannot overcome the initial hurdle of learning OpenGL / DirectX. Thus, I have prepared a short series of lectures, after which my students show quite good renderers. +![](https://haqr.eu/tinyrenderer/bresenham/bresenham0.png) -So, the task is formulated as follows: using no third-party libraries (especially graphic ones), get something like this picture: -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/africanhead.png) +## Teaser: few examples made with the renderer -_Warning: this is a training material that will loosely repeat the structure of the OpenGL library. It will be a software renderer. **I do not want to show how to write applications for OpenGL. I want to show how OpenGL works.** I am deeply convinced that it is impossible to write efficient applications using 3D libraries without understanding this._ +![](https://haqr.eu/tinyrenderer/home/demon.png) -I will try to make the final code about 500 lines. My students need 10 to 20 programming hours to begin making such renderers. At the input, we get a test file with a polygonal wire + pictures with textures. At the output, we’ll get a rendered model. No graphical interface, the program simply generates an image. +![](https://haqr.eu/tinyrenderer/home/diablo-glow.png) +![](https://haqr.eu/tinyrenderer/home/boggie.png) -Since the goal is to minimize external dependencies, I give my students just one class that allows working with [TGA](http://en.wikipedia.org/wiki/Truevision_TGA) files. It’s one of the simplest formats that supports images in RGB/RGBA/black and white formats. So, as a starting point, we’ll obtain a simple way to work with pictures. You should note that the only functionality available at the very beginning (in addition to loading and saving images) is the capability to set the color of one pixel. +![](https://haqr.eu/tinyrenderer/home/diablo-ssao.png) -There are no functions for drawing line segments and triangles. We’ll have to do all of this by hand. I provide my source code that I write in parallel with students. But I would not recommend using it, as this doesn’t make sense. The entire code is available on github, and [here](https://github.com/ssloy/tinyrenderer/tree/909fe20934ba5334144d2c748805690a1fa4c89f) you will find the source code I give to my students. +## Compilation -```C++ -#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); - image.write_tga_file("output.tga");` - return 0; -} +```sh +git clone https://github.com/ssloy/tinyrenderer.git && +cd tinyrenderer && +cmake -Bbuild && +cmake --build build -j && +build/tinyrenderer obj/diablo3_pose/diablo3_pose.obj obj/floor.obj ``` - -output.tga should look something like this: - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/reddot.png) - - -# Teaser: few examples made with the renderer - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/demon.png) - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/diablo-glow.png) - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/boggie.png) - -![](https://raw.githubusercontent.com/ssloy/tinyrenderer/gh-pages/img/00-home/diablo-ssao.png) +The rendered image is saved to `framebuffer.tga`. diff --git a/geometry.cpp b/geometry.cpp deleted file mode 100644 index 5ac61462..00000000 --- a/geometry.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "geometry.h" - -vec3 cross(const vec3 &v1, const vec3 &v2) { - return vec<3>{v1.y*v2.z - v1.z*v2.y, v1.z*v2.x - v1.x*v2.z, v1.x*v2.y - v1.y*v2.x}; -} - diff --git a/geometry.h b/geometry.h index 54e49001..1195a281 100644 --- a/geometry.h +++ b/geometry.h @@ -4,18 +4,15 @@ #include template struct vec { - vec() = default; - double & operator[](const int i) { assert(i>=0 && i=0 && i=0 && i=0 && i double operator*(const vec& lhs, const vec& rhs) { - double ret = 0; - for (int i=n; i--; ret+=lhs[i]*rhs[i]); - return ret; + double ret = 0; // N.B. Do not ever, ever use such for loops! They are highly confusing. + for (int i=n; i--; ret+=lhs[i]*rhs[i]); // Here I used them as a tribute to old-school game programmers fighting for every CPU cycle. + return ret; // Once upon a time reverse loops were faster than the normal ones, it is not the case anymore. } template vec operator+(const vec& lhs, const vec& rhs) { @@ -30,16 +27,14 @@ template vec operator-(const vec& lhs, const vec& rhs) { return ret; } -template vec operator*(const double& rhs, const vec &lhs) { +template vec operator*(const vec& lhs, const double& rhs) { vec ret = lhs; for (int i=n; i--; ret[i]*=rhs); return ret; } -template vec operator*(const vec& lhs, const double& rhs) { - vec ret = lhs; - for (int i=n; i--; ret[i]*=rhs); - return ret; +template vec operator*(const double& lhs, const vec &rhs) { + return rhs * lhs; } template vec operator/(const vec& lhs, const double& rhs) { @@ -48,51 +43,46 @@ template vec operator/(const vec& lhs, const double& rhs) { return ret; } -template vec embed(const vec &v, double fill=1) { - vec ret; - for (int i=n1; i--; ret[i]=(i vec proj(const vec &v) { - vec ret; - for (int i=n1; i--; ret[i]=v[i]); - return ret; -} - template std::ostream& operator<<(std::ostream& out, const vec& v) { for (int i=0; i struct vec<2> { - vec() = default; - vec(double x, double y) : x(x), y(y) {} + double x = 0, y = 0; double& operator[](const int i) { assert(i>=0 && i<2); return i ? y : x; } double operator[](const int i) const { assert(i>=0 && i<2); return i ? y : x; } - double norm2() const { return *this * *this; } - double norm() const { return std::sqrt(norm2()); } - vec & normalize() { *this = (*this)/norm(); return *this; } - - double x{}, y{}; }; template<> struct vec<3> { - vec() = default; - vec(double x, double y, double z) : x(x), y(y), z(z) {} + double x = 0, y = 0, z = 0; double& operator[](const int i) { assert(i>=0 && i<3); return i ? (1==i ? y : z) : x; } double operator[](const int i) const { assert(i>=0 && i<3); return i ? (1==i ? y : z) : x; } - double norm2() const { return *this * *this; } - double norm() const { return std::sqrt(norm2()); } - vec & normalize() { *this = (*this)/norm(); return *this; } +}; - double x{}, y{}, z{}; +template<> struct vec<4> { + double x = 0, y = 0, z = 0, w = 0; + double& operator[](const int i) { assert(i>=0 && i<4); return i<2 ? (i ? y : x) : (2==i ? z : w); } + double operator[](const int i) const { assert(i>=0 && i<4); return i<2 ? (i ? y : x) : (2==i ? z : w); } + vec<2> xy() const { return {x, y}; } + vec<3> xyz() const { return {x, y, z}; } }; typedef vec<2> vec2; typedef vec<3> vec3; typedef vec<4> vec4; -vec3 cross(const vec3 &v1, const vec3 &v2); + +template double norm(const vec& v) { + return std::sqrt(v*v); +} + +template vec normalized(const vec& v) { + return v / norm(v); +} + +inline vec3 cross(const vec3 &v1, const vec3 &v2) { + return {v1.y*v2.z - v1.z*v2.y, v1.z*v2.x - v1.x*v2.z, v1.x*v2.y - v1.y*v2.x}; +} template struct dt; @@ -102,50 +92,22 @@ template struct mat { vec& operator[] (const int idx) { assert(idx>=0 && idx& operator[] (const int idx) const { assert(idx>=0 && idx col(const int idx) const { - assert(idx>=0 && idx ret; - for (int i=nrows; i--; ret[i]=rows[i][idx]); - return ret; - } - - void set_col(const int idx, const vec &v) { - assert(idx>=0 && idx identity() { - mat ret; - for (int i=nrows; i--; ) - for (int j=ncols;j--; ret[i][j]=(i==j)); - return ret; - } - double det() const { return dt::det(*this); } - mat get_minor(const int row, const int col) const { - mat ret; - for (int i=nrows-1; i--; ) - for (int j=ncols-1;j--; ret[i][j]=rows[i adjugate() const { - mat ret; - for (int i=nrows; i--; ) - for (int j=ncols; j--; ret[i][j]=cofactor(i,j)); - return ret; + mat submatrix; + for (int i=nrows-1; i--; ) + for (int j=ncols-1;j--; submatrix[i][j]=rows[i+int(i>=row)][j+int(j>=col)]); + return submatrix.det() * ((row+col)%2 ? -1 : 1); } mat invert_transpose() const { - mat ret = adjugate(); - return ret/(ret[0]*rows[0]); + mat adjugate_transpose; // transpose to ease determinant computation, check the last line + for (int i=nrows; i--; ) + for (int j=ncols; j--; adjugate_transpose[i][j]=cofactor(i,j)); + return adjugate_transpose/(adjugate_transpose[0]*rows[0]); } mat invert() const { @@ -154,11 +116,16 @@ template struct mat { mat transpose() const { mat ret; - for (int i=ncols; i--; ret[i]=this->col(i)); + for (int i=ncols; i--; ) + for (int j=nrows; j--; ret[i][j]=rows[j][i]); return ret; } }; +template vec operator*(const vec& lhs, const mat& rhs) { + return (mat<1,nrows>{{lhs}}*rhs)[0]; +} + template vec operator*(const mat& lhs, const vec& rhs) { vec ret; for (int i=nrows; i--; ret[i]=lhs[i]*rhs); @@ -168,7 +135,8 @@ template vec operator*(const mat& lhs, templatemat operator*(const mat& lhs, const mat& rhs) { mat result; for (int i=R1; i--; ) - for (int j=C2; j--; result[i][j]=lhs[i]*rhs.col(j)); + for (int j=C2; j--; ) + for (int k=C1; k--; result[i][j]+=lhs[i][k]*rhs[k][j]); return result; } @@ -203,15 +171,15 @@ template std::ostream& operator<<(std::ostream& out, const return out; } -template struct dt { +template struct dt { // template metaprogramming to compute the determinant recursively static double det(const mat& src) { double ret = 0; - for (int i=n; i--; ret += src[0][i]*src.cofactor(0,i)); + for (int i=n; i--; ret += src[0][i] * src.cofactor(0,i)); return ret; } }; -template<> struct dt<1> { +template<> struct dt<1> { // template specialization to stop the recursion static double det(const mat<1,1>& src) { return src[0][0]; } diff --git a/main.cpp b/main.cpp index cd6259bc..b3994fb8 100644 --- a/main.cpp +++ b/main.cpp @@ -1,83 +1,80 @@ -#include -#include "model.h" #include "our_gl.h" +#include "model.h" -constexpr int width = 800; // output image size -constexpr int height = 800; - -const vec3 light_dir(1,1,1); // light source -const vec3 eye(1,1,3); // camera position -const vec3 center(0,0,0); // camera direction -const vec3 up(0,1,0); // camera up vector - -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<2,3> 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 Model &m) : model(m) { - uniform_l = proj<3>((ModelView*embed<4>(light_dir, 0.))).normalize(); // 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) { - varying_uv.set_col(nthvert, model.uv(iface, nthvert)); - varying_nrm.set_col(nthvert, proj<3>((ModelView).invert_transpose()*embed<4>(model.normal(iface, nthvert), 0.))); - gl_Position= ModelView*embed<4>(model.vert(iface, nthvert)); - view_tri.set_col(nthvert, proj<3>(gl_Position)); - 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) { - vec3 bn = (varying_nrm*bar).normalize(); // per-vertex normal interpolation - vec2 uv = varying_uv*bar; // tex coord interpolation - - // for the math refer to the tangent space normal mapping lecture - // https://github.com/ssloy/tinyrenderer/wiki/Lesson-6bis-tangent-space-normal-mapping - mat<3,3> AI = mat<3,3>{ {view_tri.col(1) - view_tri.col(0), view_tri.col(2) - view_tri.col(0), bn} }.invert(); - vec3 i = AI * vec3(varying_uv[0][1] - varying_uv[0][0], varying_uv[0][2] - varying_uv[0][0], 0); - vec3 j = AI * vec3(varying_uv[1][1] - varying_uv[1][0], varying_uv[1][2] - varying_uv[1][0], 0); - mat<3,3> B = mat<3,3>{ {i.normalize(), j.normalize(), bn} }.transpose(); - - vec3 n = (B * model.normal(uv)).normalize(); // transform the normal from the texture to the tangent space - double diff = std::max(0., n*uniform_l); // diffuse light intensity - vec3 r = (n*(n*uniform_l)*2 - uniform_l).normalize(); // reflected light direction, specular mapping is described here: https://github.com/ssloy/tinyrenderer/wiki/Lesson-6-Shaders-for-the-software-renderer - 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; // the pixel is not discarded + 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; } - TGAImage framebuffer(width, height, TGAImage::RGB); // the output image - lookat(eye, center, up); // build the ModelView matrix - viewport(width/8, height/8, width*3/4, height*3/4); // build the Viewport matrix - projection((eye-center).norm()); // build the Projection matrix - std::vector zbuffer(width*height, std::numeric_limits::max()); - for (int m=1; m +#include #include #include "model.h" @@ -13,23 +13,22 @@ Model::Model(const std::string filename) { char trash; if (!line.compare(0, 2, "v ")) { iss >> trash; - vec3 v; - for (int i=0;i<3;i++) iss >> v[i]; + 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; - for (int i=0;i<3;i++) iss >> n[i]; - norms.push_back(n.normalize()); + 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;i<2;i++) iss >> uv[i]; - tex_coord.push_back({uv.x, 1-uv.y}); - } else if (!line.compare(0, 2, "f ")) { - int f,t,n; + for (int i : {0,1}) iss >> uv[i]; + tex.push_back({uv.x, 1-uv.y}); + } else if (!line.compare(0, 2, "f ")) { + int f,t,n, cnt = 0; iss >> trash; - int cnt = 0; while (iss >> f >> trash >> t >> trash >> n) { facet_vrt.push_back(--f); facet_tex.push_back(--t); @@ -38,51 +37,46 @@ Model::Model(const std::string filename) { } if (3!=cnt) { std::cerr << "Error: the obj file is supposed to be triangulated" << std::endl; - in.close(); return; } } } - in.close(); - std::cerr << "# v# " << nverts() << " f# " << nfaces() << " vt# " << tex_coord.size() << " vn# " << norms.size() << std::endl; - load_texture(filename, "_diffuse.tga", diffusemap ); - load_texture(filename, "_nm_tangent.tga", normalmap ); - load_texture(filename, "_spec.tga", specularmap); + 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("_spec.tga", specularmap); } -int Model::nverts() const { - return verts.size(); -} - -int Model::nfaces() const { - return facet_vrt.size()/3; -} +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]]; } -void Model::load_texture(std::string 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; +vec4 Model::normal(const int iface, const int nthvert) const { + return norms[facet_nrm[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 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_coord[facet_tex[iface*3+nthvert]]; + 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 c0a396a0..0f7fb575 100644 --- a/model.h +++ b/model.h @@ -1,29 +1,27 @@ -#include -#include #include "geometry.h" #include "tgaimage.h" class Model { - std::vector verts{}; // array of vertices - std::vector tex_coord{}; // per-vertex array of tex coords - std::vector norms{}; // per-vertex array of normal vectors - std::vector facet_vrt{}; - std::vector facet_tex{}; // per-triangle indices in the above arrays - std::vector facet_nrm{}; - TGAImage diffusemap{}; // diffuse color texture - TGAImage normalmap{}; // normal map texture - TGAImage specularmap{}; // specular map texture - void load_texture(const std::string filename, const std::string suffix, TGAImage &img); + 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 + std::vector facet_tex = {}; // ┘ nfaces()*3 + TGAImage diffusemap = {}; // diffuse color texture + TGAImage normalmap = {}; // normal map texture + TGAImage specularmap = {}; // specular texture public: Model(const std::string filename); - int nverts() const; - int nfaces() const; - vec3 normal(const int iface, const int nthvert) const; // per triangle corner normal vertex - vec3 normal(const vec2 &uv) const; // fetch the normal vector from the normal map texture - vec3 vert(const int i) const; - vec3 vert(const int iface, const int nthvert) const; - vec2 uv(const int iface, const int nthvert) const; - const TGAImage& diffuse() const { return diffusemap; } - const TGAImage& specular() const { return specularmap; } + int nverts() const; // number of vertices + int nfaces() const; // number of triangles + 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/obj/floor_spec.tga b/obj/floor_spec.tga new file mode 100644 index 00000000..cebdcf8b Binary files /dev/null and b/obj/floor_spec.tga differ diff --git a/our_gl.cpp b/our_gl.cpp index a3c0d73f..7217015a 100644 --- a/our_gl.cpp +++ b/our_gl.cpp @@ -1,57 +1,51 @@ -#include +#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}}}; // P[1,1] = -1; does vertical flip +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 = (center-eye).normalize(); - vec3 x = cross(up,z).normalize(); - vec3 y = cross(z, x).normalize(); - mat<4,4> Minv = {{{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> Tr = {{{1,0,0,-eye.x}, {0,1,0,-eye.y}, {0,0,1,-eye.z}, {0,0,0,1}}}; - ModelView = Minv*Tr; +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 = {{embed<3>(tri[0]), embed<3>(tri[1]), embed<3>(tri[2])}}; - if (ABC.det()<1e-3) return vec3(-1,1,1); // for a degenerate triangle generate negative coordinates, it will be thrown away by the rasterizator - return ABC.invert_transpose() * embed<3>(P); +void init_zbuffer(const int width, const int height) { + zbuffer = std::vector(width*height, -1000.); } -void triangle(const vec4 clip_verts[3], IShader &shader, TGAImage &image, std::vector &zbuffer) { - vec4 pts[3] = { Viewport*clip_verts[0], Viewport*clip_verts[1], Viewport*clip_verts[2] }; // triangle screen coordinates before persp. division - vec2 pts2[3] = { proj<2>(pts[0]/pts[0][3]), proj<2>(pts[1]/pts[1][3]), proj<2>(pts[2]/pts[2][3]) }; // triangle screen coordinates after perps. division - - vec2 bboxmin( std::numeric_limits::max(), std::numeric_limits::max()); - vec2 bboxmax(-std::numeric_limits::max(), -std::numeric_limits::max()); - vec2 clamp(image.width()-1, image.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], pts2[i][j])); - bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts2[i][j])); - } +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 + + 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=(int)bboxmin.x; x<=(int)bboxmax.x; x++) { - for (int y=(int)bboxmin.y; y<=(int)bboxmax.y; y++) { - vec3 bc_screen = barycentric(pts2, vec2(x, y)); - vec3 bc_clip = vec3(bc_screen.x/pts[0][3], bc_screen.y/pts[1][3], bc_screen.z/pts[2][3]); - bc_clip = bc_clip/(bc_clip.x+bc_clip.y+bc_clip.z); // check https://github.com/ssloy/tinyrenderer/wiki/Technical-difficulties-linear-interpolation-with-perspective-deformations - double frag_depth = vec3(clip_verts[0][2], clip_verts[1][2], clip_verts[2][2])*bc_clip; - 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); + 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); + 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 7a9e4faa..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, vec2 &uvf) { + 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) = 0; + virtual std::pair fragment(const vec3 bar) const = 0; }; -void triangle(const vec4 clip_verts[3], 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 ebbca767..801c6f37 100644 --- a/tgaimage.cpp +++ b/tgaimage.cpp @@ -2,20 +2,22 @@ #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(&header), sizeof(header)); if (!in.good()) { - in.close(); std::cerr << "an error occured while reading the header\n"; return false; } @@ -23,7 +25,6 @@ bool TGAImage::read_tga_file(const std::string filename) { h = header.height; bpp = header.bitsperpixel>>3; if (w<=0 || h<=0 || (bpp!=GRAYSCALE && bpp!=RGB && bpp!=RGBA)) { - in.close(); std::cerr << "bad bpp (or width/height) value\n"; return false; } @@ -32,18 +33,15 @@ bool TGAImage::read_tga_file(const std::string filename) { if (3==header.datatypecode || 2==header.datatypecode) { in.read(reinterpret_cast(data.data()), nbytes); if (!in.good()) { - in.close(); std::cerr << "an error occured while reading the data\n"; return false; } } else if (10==header.datatypecode||11==header.datatypecode) { if (!load_rle_data(in)) { - in.close(); std::cerr << "an error occured while reading the data\n"; return false; } } else { - in.close(); std::cerr << "unknown file format " << (int)header.datatypecode << "\n"; return false; } @@ -52,7 +50,6 @@ bool TGAImage::read_tga_file(const std::string filename) { if (header.imagedescriptor & 0x10) flip_horizontally(); std::cerr << w << "x" << h << "/" << bpp*8 << "\n"; - in.close(); return true; } @@ -110,59 +107,35 @@ bool TGAImage::write_tga_file(const std::string filename, const bool vflip, cons constexpr std::uint8_t extension_area_ref[4] = {0, 0, 0, 0}; constexpr std::uint8_t footer[18] = {'T','R','U','E','V','I','S','I','O','N','-','X','F','I','L','E','.','\0'}; std::ofstream out; - out.open (filename, std::ios::binary); + out.open(filename, std::ios::binary); if (!out.is_open()) { std::cerr << "can't open file " << filename << "\n"; - out.close(); return false; } - TGAHeader header; + TGAHeader header = {}; header.bitsperpixel = bpp<<3; header.width = w; header.height = h; - header.datatypecode = (bpp==GRAYSCALE?(rle?11:3):(rle?10:2)); + header.datatypecode = (bpp==GRAYSCALE ? (rle?11:3) : (rle?10:2)); header.imagedescriptor = vflip ? 0x00 : 0x20; // top-left or bottom-left origin out.write(reinterpret_cast(&header), sizeof(header)); - if (!out.good()) { - out.close(); - std::cerr << "can't dump the tga file\n"; - return false; - } + if (!out.good()) goto err; if (!rle) { out.write(reinterpret_cast(data.data()), w*h*bpp); - if (!out.good()) { - std::cerr << "can't unload raw data\n"; - out.close(); - return false; - } - } else if (!unload_rle_data(out)) { - out.close(); - std::cerr << "can't unload rle data\n"; - return false; - } + if (!out.good()) goto err; + } else if (!unload_rle_data(out)) goto err; out.write(reinterpret_cast(developer_area_ref), sizeof(developer_area_ref)); - if (!out.good()) { - std::cerr << "can't dump the tga file\n"; - out.close(); - return false; - } + if (!out.good()) goto err; out.write(reinterpret_cast(extension_area_ref), sizeof(extension_area_ref)); - if (!out.good()) { - std::cerr << "can't dump the tga file\n"; - out.close(); - return false; - } + if (!out.good()) goto err; out.write(reinterpret_cast(footer), sizeof(footer)); - if (!out.good()) { - std::cerr << "can't dump the tga file\n"; - out.close(); - return false; - } - out.close(); + if (!out.good()) goto err; return true; +err: + std::cerr << "can't dump the tga file\n"; + return false; } -// TODO: it is not necessary to break a raw chunk for two equal pixels (for the matter of the resulting size) bool TGAImage::unload_rle_data(std::ofstream &out) const { const std::uint8_t max_chunk_length = 128; size_t npixels = w*h; @@ -188,24 +161,20 @@ bool TGAImage::unload_rle_data(std::ofstream &out) const { run_length++; } curpix += run_length; - out.put(raw?run_length-1:run_length+127); - if (!out.good()) { - std::cerr << "can't dump the tga file\n"; - return false; - } + out.put(raw ? run_length-1 : run_length+127); + if (!out.good()) return false; out.write(reinterpret_cast(data.data()+chunkstart), (raw?run_length*bpp:bpp)); - if (!out.good()) { - std::cerr << "can't dump the tga file\n"; - return false; - } + if (!out.good()) return false; } return true; } TGAColor TGAImage::get(const int x, const int y) const { - if (!data.size() || x<0 || y<0 || x>=w || y>=h) - return {}; - return TGAColor(data.data()+(x+y*w)*bpp, bpp); + if (!data.size() || x<0 || y<0 || x>=w || y>=h) return {}; + TGAColor ret = {0, 0, 0, 0, bpp}; + const std::uint8_t *p = data.data()+(x+y*w)*bpp; + for (int i=bpp; i--; ret.bgra[i] = p[i]); + return ret; } void TGAImage::set(int x, int y, const TGAColor &c) { @@ -214,17 +183,15 @@ void TGAImage::set(int x, int y, const TGAColor &c) { } void TGAImage::flip_horizontally() { - int half = w>>1; - for (int i=0; i>1; for (int i=0; i data = {}; };