Tiny Renderer2(绘制网格)

2021-02-01  本文已影响0人  烂醉花间dlitf

介绍

简单的画一条线

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 0.123 0.234 0.345 1.0
vt 0.500 1
vn 0.707 0.000 0.707

画网格

首先需要一个类去解析 .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

上一篇下一篇

猜你喜欢

热点阅读