Featured image of post myLittleRender01-直线与线框

myLittleRender01-直线与线框

Learn and Write Code of tinyrenderer 01 lesson

前言

本人打算提高个人的C++基础编程水平和图形学理解,所以再再再次启动了tinyrenderer的程序编写,相较于之前被动的抄写代码,我准备选择直接观察他的代码结果后根据自己的理解进行编写,并进行结果比较。 开始编写前,思考出如图所示的基础架构(如果这可以称为架构的话)relation.png 项目结构如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.
├── CMakeLists.txt
├── geometry.cpp
├── geometry.h
├── main.cpp
├── model.cpp
├── model.h
├── obj
│   └── african_head.obj
├── rasterizer.cpp
├── rasterizer.h
├── README.md
├── sandbox.cpp
├── tgaimage.cpp
└── tgaimage.h

源码可以在这里看到 tgaimage.h和tgaimage.cpp复制自ssloy的初始提交,可以在lesson01的wiki看到,在此不多赘述。

基础设施建设

向量类

在GAMES101的作业中,使用了线性代数库 Eigen,但是对于本环境的代码来说,并不需要这么强大的功能,所以参考原仓库手搓了一份向量类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>  
struct Vec2{  
    union{  
        struct{T x, y;};  
        struct{T u, v;};  
    };  
    Vec2(): x(0), y(0) {}  
    Vec2(T _x, T _y): x(_x), y(_y) {}  
};

template <typename T>  
struct Matrix4{  
    std::array<T,16> data;  
    Matrix4():data({0}){}  
    T get_index(int i, int j) const{  
        return data[i * 4 + j];  
    }      
    Matrix4(std::initializer_list<T> list) requires requires{list.size() == 16;}{  
        int i = 0;  
        for(auto it = list.begin(); it != list.end(); it++){  
            data[i++] = *it;  
        }  
    }  
};

仅做举例,关于重载运算符等操作不多赘述。
其中,使用 union 内的 匿名 struct 达到了.x.u 访问同一内存的效果。 Matrix4(std::initializer_list<T> list) requires requires{list.size() == 16; 的写法来自软研C++组的支持, requires requires{list.size() == 16} 表明在构造函数时必须调用一个16个元素的列表。
Matrix 结构体尚未经过测试,可能存在bug。

Model类

头文件定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Model{  
  public:  
    Model() = delete;  
    explicit Model(const std::string& filename);  
    [[nodiscard]] std::vector<Vec3f> getVerts() const {return verts_;}  
    [[nodiscard]] std::vector<Vec2f> getUV() const {return uv_;}  
    [[nodiscard]] std::vector<Vec3f> getNorms() const {return norms_;}  
    [[nodiscard]] std::vector<std::array<std::array<unsigned short,3>
, 3>> getFaces() const {return faces_;}  
  private:  
    std::vector<Vec3f> verts_;  
    std::vector<Vec2f> uv_;  
    std::vector<Vec3f> norms_;  
    std::vector<std::array<std::array<unsigned short,3>, 3>> faces_;  
  
};

wavefront obj 文件是由wavefront 公司所创建的模型格式,受到各类建模软件的广泛支持。该模型会在行首声明数据类型

  • v代表vertice,顶点
  • vt 代表vertice texcoord,纹理坐标
  • vn代表nromal,法线
  • f代表face,即面
    face应该是最特殊的行数,下面是一个示例
1
2
3
f 321/304/321 318/302/318 147/127/147  
f 456/446/456 321/304/321 525/517/525  
f 456/446/456 525/517/525 457/447/457  

数据每一行分成了三部分,每一部分都代表了一个顶点数据,分别代表顶点索引,贴图索引和法线索引,即

1
vert/texture/normal   vert/texture/normal   vert/texture/normal

所以使用了 std::vector<std::array<std::array<unsigned short, 3>, 3>> 这种臃肿的数据结构。

构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Model::Model(const std::string &filename) {  
  std::ifstream file(filename);  
  if ( !file.is_open()) {  
    std::cerr << "Error opening file " << filename << std::endl;  
    return;  
  }  
  std::string line;  
  while (std::getline(file, line)){  
    std::istringstream iss(line);  
    std::string type;  
    iss >> type;  
    if(type == "v") {  
      Vec3f v;  
      iss >> v.x >> v.y >> v.z;  
      verts_.push_back(v);  
    }else if(type == "vt"){  
        Vec2f vt;  
        iss >> vt.x >> vt.y;  
        uv_.push_back(vt);  
    }  
    else if (type == "vn"){  
        Vec3f vn;  
        iss >> vn.x >> vn.y >> vn.z;  
        norms_.push_back(vn);  
    }  
    else if (type == "f"){  
      std::array<std::array<unsigned short, 3>, 3> face{};  
      char slash;  
      for (int i = 0; i < 3; i++) {  
        iss >> face[i][0] >> slash >> face[i][1] >> slash >> face[i][2];  
      }  
      faces_.push_back(face);  
    }  
  
  }  
  file.close();  
  std::cout << "Loaded" << verts_.size() << " vertices" << std::endl;  
  std::cout << "Loaded" << uv_.size() << " uv" << std::endl;  
  std::cout << "Loaded" << norms_.size() << " normals" << std::endl;  
  std::cout << "Loaded" << faces_.size() << " faces" << std::endl;  
  
}

构造函数会打开文件后按行读取,根据行首的类型压入对应的动态数组,并在读取完后输出消息读取数量。(不知道为什么,switch语句不支持string类型,所以使用连续的 ifelse 进行逻辑判断。)

布雷森汉姆直线算法

布雷森汉姆的核心思想是通过整形的计算代替浮点数的计算,获得更好的性能。
对于斜率大于1或斜率为负数这类情况,布雷森汉姆算法会交换起始点,由x步进改为y步进等方法保证适用于各种情况。算法实现来自GAMES101的作业实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
void line(const Vec2i p1, const Vec2i p2, TGAImage &image,
		  const TGAColor& color) {  
  int x1 = p1.x;  
  int y1 = p1.y;  
  int x2 = p2.x;  
  int y2 = p2.y;  
  
  int x, y, xe, ye ;  
  int dx = x2 - x1;  
  int dy = y2 - y1;  
  int dx1 = std::abs(dx);  
  int dy1 = std::abs(dy);  
  int px = 2 * dy1 - dx1;  
  int py = 2 * dx1 - dy1;  
    if (dy1 <= dx1) {  
      if (dx >= 0) {  
        x = x1;  
        y = y1;  
        xe =x2;  
      }else {  
        x =x2;  
        y = y2;  
        xe = x1;  
      }  
      image.set(x, y, color);  
      for (int i = 0; x < xe; i++) {  
        x = x + 1;  
        if (px < 0) {  
          px = px + 2 * dy1;  
        }else {  
          if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {  
            y = y + 1;  
          }else {  
            y = y - 1;  
          }  
          px = px + 2 * (dy1 - dx1);  
        }  
        image.set(x, y, color);  
      }  
    }else {  
      if (dy >= 0) {  
        x = x1;  
        y = y1;  
        ye = y2;  
      }else {  
        x = x2;  
        y = y2;  
        ye = y1;  
      }  
      image.set(x, y, color);  
      for (int i = 0; y < ye; i++) {  
        y = y + 1;  
        if (py <= 0) {  
          py = py + 2 * dx1;  
        }else {  
          if ((dx < 0 && dy < 0) || (dx > 0 && dy > 0)) {  
            x = x + 1;  
          }else {  
            x = x - 1;  
          }  
          py = py + 2 * (dx1 - dy1);  
        }  
        image.set(x, y, color);  
      }  
    }  
}

绘图

此时我们已经准备好了所有基本设备,准备好测试文件 sandbox.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "rasterizer.h"  
#include "tgaimage.h"  
#include "common.h"  
#include "model.h"  
  
  
  
int main() {  
  TGAImage image(800, 800, TGAImage::RGB);  
  const Model model("../../obj/african_head.obj");  
  const auto modelFace = model.getFaces();  
  const auto modelVerts = model.getVerts();  
  for (int i=0; i<model.getFaces().size(); ++i) {  
    auto face = modelFace[i];  
    for (int j=0; j<3; ++j) {  
      Vec3f v0 = modelVerts[face[j][0]-1];  
      Vec3f v1 = modelVerts[face[(j+1)%3][0]-1];  
      line({(int)((v0.x+1.)*image.get_width()/2.), (int)((v0.y+1.)*image.get_height()/2.)},  
           {(int)((v1.x+1.)*image.get_width()/2.), (int)((v1.y+1.)*image.get_height()/2.)},  
           image, white);  
    }  
  }  
  image.flip_vertically();  
  image.write_tga_file("lesson1.tga");  
  return 0;  
}

画出封面图head