美文网首页
Tiny Renderer2(绘制网格)

Tiny Renderer2(绘制网格)

作者: 烂醉花间dlitf | 来源:发表于2021-02-01 13:13 被阅读0次

    介绍

    简单的画一条线

    void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
        for (float t=0.; t<1.; t+=.01) { 
            int x = x0 + (x1-x0)*t; 
            int y = y0 + (y1-y0)*t; 
            image.set(x, y, color); 
        } 
    }
    

    这个方法其实跟差值差不多,t 取 100 份,然后从起始点走平均的画一百个点到结束点,但这样对于很短的线段来说,就比较浪费,他们可能只需要10个点就能画出很流畅的线。


    一百个点

    按像素来画一条线

    void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
        for (int x=x0; x<=x1; x++) { 
            float t = (x-x0)/(float)(x1-x0); 
            // int y = y0*(1.-t) + y1*t; 
            int y = y0 + (y1 - y0) * t;
            image.set(x, y, color); 
        } 
    }
    

    这个方法是从 x0 出发,然后在 x0+1 的时候,算出 y 的值,然后画一个点;然后在 x0+2 的时候算出 y 的值,再画一个点......这种方式对于相对是躺着的线来说比较友好(line1),因为他的 x 轴像素是连续的,但对于站着的线就不太 ok(line2)。而且对于从右往左走的线就根本画不出来(line3),因为这个 for 循环是递增的。

    line(13, 20, 80, 40, image, white);   // line1
    line(20, 13, 40, 80, image, red);      // line2 
    line(80, 40, 13, 20, image, red);      // line3
    
    line3 没有

    修改按像素来画一条线的 bug

    如果能将所有的线都变成上面的 line1 一样不就好了?代码如下:

    void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
        bool steep = false; 
        if (std::abs(x0-x1)<std::abs(y0-y1)) { // if the line is steep, we transpose the image 
            std::swap(x0, y0); 
            std::swap(x1, y1); 
            steep = true; 
        } 
        if (x0>x1) { // make it left−to−right 
            std::swap(x0, x1); 
            std::swap(y0, y1); 
        } 
        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); 
            } 
        } 
    }
    

    第一个 if 是防止上面 line2 的情况,先判断线的斜率,以左下角为原点建立平面直角坐标系,直线的斜率大于 1 的话(也就是跟 x 轴的夹角大于 45°),就使它关于 y=x 这条线对称,在代码上来看就是交换起点的 x 和 y,交换终点的 x 和 y,把两个点给对称了,整条线自然是关于 y=x 对称的了。如果对称了需要把 steep 设为 true。
    第二个 if 是防止上面 line3 的情况,此刻我们的线都是斜率小于 1 的了,但不能让 x1 大于 x0,所以如果 x1 大于 x0 了,那就交换终点和起点的位置,这个不需要记录,因为对后续不产生影响。
    下面一个 for 循环跟上面一样,需要注意的就是如果 steep 为 true,说明之前是根据 y=x 对称过的了,需要给他对称回来。

    优化(去除法)

    作者是用的 Linux 平台下的一个性能分析工具 gprof,分析出 70% 的时间都花费在了 line 函数上面,分析一下 line 函数里面还有什么地方可以优化,可以看到 x1-x0 其实是固定的,不需要每次都在 for 里面计算一次。所以单独提出:

        int dx = x1-x0; 
        int dy = y1-y0; 
        float derror = std::abs(dy/float(dx)); 
    

    derror 就是每次 x+1 之后,y 应该加的分量。比如说 derror = 0.4,那么在绘制完 (x0,y0) 这个像素之后,下一个绘制的点应该是 (x0+1,y0+0.4),假设 (x0,y0) 的坐标在 (0.5,0.5),那么 (x0+1,y0+0.4) 应该是 (1.5,0.9),可以看到 y 的坐标还在第一个像素点里面,所以 y 不变,也就是第二个点绘制 (1.5,0.5) 这个像素,然后来到第三个点,应该绘制 (2.5,1.3) ,可以看到 1.3 已经是属于下一个 y 轴的像素了,所以将 y+1,也就是绘制 (2.5,1.5) 这个点......所以可以看到当 y 大于 0.5,1.5,2.5,3.5...... 的时候需要进行 +1,或者 -1,那么在每次比较之后都减去 1 ,就可以每次都只和 0.5 进行比较了。完整代码如下:

    void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
        bool steep = false; 
        if (std::abs(x0-x1)<std::abs(y0-y1)) { 
            std::swap(x0, y0); 
            std::swap(x1, y1); 
            steep = true; 
        } 
        if (x0>x1) { 
            std::swap(x0, x1); 
            std::swap(y0, y1); 
        } 
        int dx = x1-x0; 
        int dy = y1-y0; 
        float derror = std::abs(dy/float(dx)); 
        float error = 0; 
        int y = y0; 
        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) { 
                y += (y1>y0?1:-1); 
                error -= 1.; 
            } 
        } 
    } 
    

    for 里面已经没有除法了,但是还有浮点数。

    优化(去浮点数)

    怎么去浮点数去掉呢,其实使用浮点数主要是因为有个 0.5 的比较,那直接将 0.5*2就可以啦,那相应的 error 也应该 *2,但是 error 是 derror 的和,所以将 derror *2 就可以了,然后就会发现 std::abs((2*dy)/float(dx)) 依然是一个 浮点数,所以再乘分母,也就是 dx,最后结果就是:

    void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
        bool steep = false; 
        if (std::abs(x0-x1)<std::abs(y0-y1)) { 
            std::swap(x0, y0); 
            std::swap(x1, y1); 
            steep = true; 
        } 
        if (x0>x1) { 
            std::swap(x0, x1); 
            std::swap(y0, y1); 
        } 
        int dx = x1-x0; 
        int dy = y1-y0; 
        int derror2 = std::abs(dy)*2;  // 原来的乘了 2dx
        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) {  // 原来的 0.5 乘了 2dx
                y += (y1>y0?1:-1); 
                error2 -= dx*2;  // 原来的 1 乘了 2dx
            } 
        } 
    } 
    

    .obj 格式

    具体可以参考:https://en.wikipedia.org/wiki/Wavefront_.obj_file

    • # 后面都是注释
    # 这是一个注释
    
    • v 后面是一个顶点,可以是 xyz,也可以是 xyzw
    v 0.123 0.234 0.345 1.0
    
    • vt 是 UV
    vt 0.500 1
    
    • vn 是顶点法线,必须是单位向量
    vn 0.707 0.000 0.707
    
    • f 是多边形面
      • 只有顶点的多边形面,顶点数量可以大于等于 3,比如说 f 1 2 3 4... 就是说,以刚刚 v 开头的顶点列表的第一个、第二个、第三个、第四个(注意索引是从 1 开始,不是 0)顶点坐标为顶点建立多边形面
      • 有 UV 的多边形面,比如 f 1/5 2/6 3/6 4/5 ... 就是说用顶点列表的第一个、第二个、第三个、第四个建立多边形面,并且第一个顶点对应的 UV 是 UV 列表(就是刚刚以 vt 开头的那些)中的第五个坐标,第二个顶点对应的 UV 列表中的第六个坐标......
      • 有 UV 和法线的多边形面,比如 f 1/5/7 2/6/8 3/6/9 4/5/10 ... 同理,第一个顶点的坐标是顶点列表中的第一个,UV 是 UV 列表中的第五个,法线是法线列表(也就是 vn 开头的一列)中的第七个。
      • 只有法线,没有 UV 的多边形面,f 1//7 2//8 3//9 4//10 ... 直接将 UV 删掉就好了。

    画网格

    首先需要一个类去解析 .obj 文件,可以直接把下面的几个文件复制进自己的工程:


    目录结构
    // file_name:model.h
    #ifndef __MODEL_H__
    #define __MODEL_H__
    
    #include <vector>
    #include "geometry.h"
    
    class Model {
    private:
        std::vector<Vec3f> verts_;
        std::vector<std::vector<int> > faces_;
    public:
        Model(const char* filename);
        ~Model();
        int nverts();
        int nfaces();
        Vec3f vert(int i);
        std::vector<int> face(int idx);
    };
    
    #endif //__MODEL_H__
    
    // file_name :model.cpp
    #include <iostream>
    #include <string>
    #include <fstream>
    #include <sstream>
    #include <vector>
    #include "model.h"
    
    Model::Model(const char* filename) : verts_(), faces_() {
        std::ifstream in;
        in.open(filename, std::ifstream::in);
        if (in.fail()) return;
        std::string line;
        while (!in.eof()) {
            std::getline(in, line);
            std::istringstream iss(line.c_str());
            char trash;
            if (!line.compare(0, 2, "v ")) {
                iss >> trash;
                Vec3f v;
                for (int i = 0; i < 3; i++) iss >> v.raw[i];
                verts_.push_back(v);
            }
            else if (!line.compare(0, 2, "f ")) {
                std::vector<int> f;
                int itrash, idx;
                iss >> trash;
                while (iss >> idx >> trash >> itrash >> trash >> itrash) {
                    idx--; // in wavefront obj all indices start at 1, not zero
                    f.push_back(idx);
                }
                faces_.push_back(f);
            }
        }
        std::cerr << "# v# " << verts_.size() << " f# " << faces_.size() << std::endl;
    }
    
    Model::~Model() {
    }
    
    int Model::nverts() {
        return (int)verts_.size();
    }
    
    int Model::nfaces() {
        return (int)faces_.size();
    }
    
    std::vector<int> Model::face(int idx) {
        return faces_[idx];
    }
    
    Vec3f Model::vert(int i) {
        return verts_[i];
    }
    
    // file_name :geometry.h
    #ifndef __GEOMETRY_H__
    #define __GEOMETRY_H__
    
    #include <cmath>
    
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    template <class t> struct Vec2 {
        union {
            struct { t u, v; };
            struct { t x, y; };
            t raw[2];
        };
        Vec2() : u(0), v(0) {}
        Vec2(t _u, t _v) : u(_u), v(_v) {}
        inline Vec2<t> operator +(const Vec2<t>& V) const { return Vec2<t>(u + V.u, v + V.v); }
        inline Vec2<t> operator -(const Vec2<t>& V) const { return Vec2<t>(u - V.u, v - V.v); }
        inline Vec2<t> operator *(float f)          const { return Vec2<t>(u * f, v * f); }
        template <class > friend std::ostream& operator<<(std::ostream& s, Vec2<t>& v);
    };
    
    template <class t> struct Vec3 {
        union {
            struct { t x, y, z; };
            struct { t ivert, iuv, inorm; };
            t raw[3];
        };
        Vec3() : x(0), y(0), z(0) {}
        Vec3(t _x, t _y, t _z) : x(_x), y(_y), z(_z) {}
        inline Vec3<t> operator ^(const Vec3<t>& v) const { return Vec3<t>(y * v.z - z * v.y, z * v.x - x * v.z, x * v.y - y * v.x); }
        inline Vec3<t> operator +(const Vec3<t>& v) const { return Vec3<t>(x + v.x, y + v.y, z + v.z); }
        inline Vec3<t> operator -(const Vec3<t>& v) const { return Vec3<t>(x - v.x, y - v.y, z - v.z); }
        inline Vec3<t> operator *(float f)          const { return Vec3<t>(x * f, y * f, z * f); }
        inline t       operator *(const Vec3<t>& v) const { return x * v.x + y * v.y + z * v.z; }
        float norm() const { return std::sqrt(x * x + y * y + z * z); }
        Vec3<t>& normalize(t l = 1) { *this = (*this) * (l / norm()); return *this; }
        template <class > friend std::ostream& operator<<(std::ostream& s, Vec3<t>& v);
    };
    
    typedef Vec2<float> Vec2f;
    typedef Vec2<int>   Vec2i;
    typedef Vec3<float> Vec3f;
    typedef Vec3<int>   Vec3i;
    
    template <class t> std::ostream& operator<<(std::ostream& s, Vec2<t>& v) {
        s << "(" << v.x << ", " << v.y << ")\n";
        return s;
    }
    
    template <class t> std::ostream& operator<<(std::ostream& s, Vec3<t>& v) {
        s << "(" << v.x << ", " << v.y << ", " << v.z << ")\n";
        return s;
    }
    
    #endif //__GEOMETRY_H__
    

    geometry.h

    定义了四个类型:浮点型的二维向量(Vec2f),整型的二维向量(Vec2i),浮点型的三维向量(Vec3f),整型的三维向量(Vec3i),并且重载了一些常用的运行符,比如 + - * ^ <<等,还有求膜,单位化向量等。

    modle.h

    将 .obj 文件解析,在构造函数中传入 .obj 文件的地址,然后将里面的顶点都放到 std::vector<Vec3f> verts_; 中(这里就默认不管 w 了), 将 .obj 文件中的多边形面都放到 std::vector<std::vector<int> > faces_; 中,每一行都是一个 std::vector<int>,并且只留了顶点的信息,UV 和法线都被舍弃了,比如 f 24/1/24 25/2/25 26/3/26 在 vector<int> 中的存储的就是 23,24,25(因为 obj 中索引是从 1 ,但是数组索引是从 0,所以都减了一)。

    下面是 main:

    #include <vector>
    #include <cmath>
    #include "tgaimage.h"
    #include "model.h"
    #include "geometry.h"
    #include "iostream"
    
    const TGAColor white = TGAColor(255, 255, 255, 255);
    const TGAColor red = TGAColor(255, 0, 0, 255);
    Model* model = NULL;
    const int width = 800;
    const int height = 800;
    
    void line(int x0, int y0, int x1, int y1, TGAImage& image, TGAColor color) {
        bool steep = false;
        if (std::abs(x0 - x1) < std::abs(y0 - y1)) {
            std::swap(x0, y0);
            std::swap(x1, y1);
            steep = true;
        }
        if (x0 > x1) {
            std::swap(x0, x1);
            std::swap(y0, y1);
        }
    
        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);
            }
            else {
                image.set(x, y, color);
            }
        }
    }
    
    int main(int argc, char** argv) {
        if (2 == argc) { // 如果运行时有参数,就使用参数为 obj 的路径
            model = new Model(argv[1]);
        }
        else {  // 不然就使用 head.obj 为路径名
            model = new Model("Garen.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]); // 循环画线 0-1,1-2,2-0
                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);
            }
        }
    
        image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
        image.write_tga_file("output.tga");
        delete model;
        return 0;
    }
    
    

    可能会有人对 int x0 = (v0.x + 1.) * width / 2.;这系列操作有疑问,这是因为对于作者的 obj 文件,顶点是在 [-1,1] 之间的,所以他将其 +1,也就是把范围映射在了 [0,1] 之间,再 * width(height ) / 2 进行放大,这样的话,就会画在画布中间了。
    但需要注意的是顶点是在 [-1,1] 之间并不是硬性规定,比如我自己在网上下的另一个模型,内容就是这样的:

    盖伦模型
    当我直接使用
                int x0 = v0.x;
                int y0 = v0.y;
                int x1 = v1.x;
                int y1 = v1.y;
    

    的方式去绘制时,结果是这样:


    盖伦

    完整画出来应该是这样:


    完整的
    所以如何将一个 obj 画在画布中间还是需要自己将范围进行映射。比如记录一下 x,y 的最大值,然后将坐标先除以最大值,这样都映射在了 [-1,1] 之间,再按照上面的方法绘制。

    教程

    https://github.com/ssloy/tinyrenderer/wiki/Lesson-1-Bresenham%E2%80%99s-Line-Drawing-Algorithm

    相关文章

      网友评论

          本文标题:Tiny Renderer2(绘制网格)

          本文链接:https://www.haomeiwen.com/subject/mpfktltx.html