500行C++代码实现软件渲染器 - 1.Bresenham直线绘制算法

4 篇文章 0 订阅
4 篇文章 0 订阅

第一版

这一课的目标是绘制线框。为了实现这个目标,我们需要先学会绘制线段。我们可以直接阅读Bresenham直线算法的内容,但是我们还是自己写代码实现。要实现点(x0,y0)和(x1,y1)之间线段的绘制,代码应该怎么写呢?显然,代码可能是这样:

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*(1.-t) + x1*t; 
        int y = y0*(1.-t) + y1*t; 
        image.set(x, y, color); 
    } 
}

直线绘制效果如下,完整代码在此。

第二版

先不论效率,第一版代码的问题还在于常量的选择,代码中该常量等于0.01。如果我们把常量设置为0.1,我们绘制的线段将会变成这样:

我们会轻易的发现必要的步骤:把常量设置为需要绘制的像素数量。那么,简单但不正确的改进代码看起来会是这样:

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; 
        image.set(x, y, color); 
    } 
}

注意!在我的学生的代码中,第一个代码错误就来源于整数的除法,如 (x-x0)/(x1-x0)。如果我们尝试以下代码绘制一些线段:

line(13, 20, 80, 40, image, white); 
line(20, 13, 40, 80, image, red); 
line(80, 40, 13, 20, image, red);

效果如下:

结果发现第一个线段是正确的,第二条线段有洞,而第三条线段根本没有绘制出来。注意,第一行代码和第三行代码绘制了同一条线段,只是颜色不同,方向相反。我们已经看到了白色的线段,它很好的被绘制出来了。我们希望将白色线段改成红色,但是却没有实现。这是为了测试对称性:也就是说线段的绘制结果不应该依赖与端点的顺序,线段(a,b)必须和线段(b,a)完全一样。

第三版

我们将两个点的坐标交换,保证x0始终小于x1,以此来解决第二版中红色线段未能绘制的问题。

而其中一条线段中间有洞,是因为线段的高度大于线段的宽度。我的学生经常建议通过以下方法来解决:

if (dx>dy) {for (int x)} else {for (int y)}

不会吧!

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); 
        } 
    } 
}

第四版-控制时间

提醒:编译器的优化(g++ -O3)往往比你优化代码的效果更好。这一节的内容的存在是因为历史原因。

第三版的代码效果很好,复杂度也正是实现最终渲染器所想要的。虽然它的效率显然是不高的,但是代码简洁、可读性强。同时注意,代码中也没有断言和边界检查,这很糟糕。在这个系列文章中,我不会重载这些代码,因为这些代码正被广泛阅读。同时,我会对代码检查进行系统地提醒。

因此,随然第三版代码能很好运行,但是我们仍然可以优化它。优化是一件危险的事情。我们需要清楚代码运行的平台。针对图形卡进行优化和针对CPU进行优化是完全不同的两件事情。在开展优化之前,我们必须对代码进行分析。并试图猜测,哪些操作是对资源消耗比较敏感的。

为了测试,我将之前的三条线段绘制了一百万次。我的CPU是Intel® Core(TM) i5-3450 CPU @ 3.10GHz,对于每一个像素代码都会调用TGAColor的拷贝构造函数。总共大概会有1000000*3*50个像素。调用次数很多,对吧?那么从哪里开始优化呢?分析结果会告诉我们。

我采用g++ -ggdb -g -pg -O0编译代码,然后运行gprof,结果如下:

%   cumulative   self              self     total 
 time   seconds   seconds    calls  ms/call  ms/call  name 
 69.16      2.95     2.95  3000000     0.00     0.00  line(int, int, int, int, TGAImage&, TGAColor) 
 19.46      3.78     0.83 204000000     0.00     0.00  TGAImage::set(int, int, TGAColor) 
  8.91      4.16     0.38 207000000     0.00     0.00  TGAColor::TGAColor(TGAColor const&) 
  1.64      4.23     0.07        2    35.04    35.04  TGAColor::TGAColor(unsigned char, unsigned char, unsigned char, unsigned char) 
  0.94      4.27     0.04                             TGAImage::get(int, int)

10%的时间花费在复制颜色上了,但是70%的时间花费在调用line()方法上了。那么这个方法就是我们需要优化的。

第四版-继续

我们注意到for循环中的除运算每次都是一样的,我们把它移到循环体外面。error变量记录了从当前(x,y)坐标到最佳线段的距离。每一次error大于一个像素,我们把y增加一,同时吧error减去一。

源代码在此:

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.; 
        } 
    } 
} 

这是gprof的输出结果:

%   cumulative   self              self     total 
 time   seconds   seconds    calls  ms/call  ms/call  name 
 38.79      0.93     0.93  3000000     0.00     0.00  line(int, int, int, int, TGAImage&, TGAColor) 
 37.54      1.83     0.90 204000000     0.00     0.00  TGAImage::set(int, int, TGAColor) 
 19.60      2.30     0.47 204000000     0.00     0.00  TGAColor::TGAColor(int, int) 
  2.09      2.35     0.05        2    25.03    25.03  TGAColor::TGAColor(unsigned char, unsigned char, unsigned char, unsigned char) 
  1.25      2.38     0.03                             TGAImage::get(int, int) 

第五版-最终版

为什么我们一定要使用浮点数呢?唯一的原因是我们需要使用1除以dx并在循环体内与0.5进行比较。我们可以避免使用浮点数,把error变量替换为另外一个。我们称之为error2,并且假定它等于error*dx*2。代码在此:

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; 
    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) { 
            y += (y1>y0?1:-1); 
            error2 -= dx*2; 
        } 
    } 
} 
%   cumulative   self              self     total 
 time   seconds   seconds    calls  ms/call  ms/call  name 
 42.77      0.91     0.91 204000000     0.00     0.00  TGAImage::set(int, int, TGAColor) 
 30.08      1.55     0.64  3000000     0.00     0.00  line(int, int, int, int, TGAImage&, TGAColor) 
 21.62      2.01     0.46 204000000     0.00     0.00  TGAColor::TGAColor(int, int) 
  1.88      2.05     0.04        2    20.02    20.02  TGAColor::TGAColor(unsigned char, unsigned char, unsigned char, unsigned char) 

现在,我们可以将不需要的副本删掉了,通过引用传递颜色进行调用就行了。最终版本代码里面,没有一个乘法和除法。最终执行时间从2.95秒下降到0.64秒。

线框渲染

现在我们可以准备完成线框的渲染了。你可以在这里查看源代码和测试模型。我使用了obj格式文件存储模型信息。我们的渲染器需要的信息是从顶点数组中读取出来的,格式如下:

v 0.608654 -0.568839 -0.416318

每一个顶点xyz坐标占一行,三角面的顶点信息如下:

f 1193/1240/1193 1180/1227/1180 1179/1226/1179

每个空格之后数字表示顶点数组中读取顶点的序号。这就表示,顶点序号为1193、1180、1179的顶点组成一个三角形。源代码中model.cpp包含了一个简单的转换器。通过以下main.cpp中的循环,我们的线框模型就能绘制出来了。

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]); 
        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); 
    } 
}

下一次,我们将绘制二维的三角形以提升我们的渲染器效果。

感谢原作者Dmitry V. Sokolov的授权,原文链接:https://github.com/ssloy/tinyrenderer/wiki/Lesson-1:-Bresenham%E2%80%99s-Line-Drawing-Algorithm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值