从零开始手写渲染器(1 绘制直线)---tinyrenderer中文翻译+个人理解

目录

Lesson 1: Bresenham画线算法

方案一

方案二

方案三

方案四/最后一个方案

 方案五的解释


Lesson 1: Bresenham画线算法

在一个二维空间中,如何绘制一条线段从v0点(x0,y0)到v1点(x1,y1)呢?

  • 在一条一维的直线上,我们可以在走的过程中记录下走了的距离和总距离的比例t(比如我们走了十分之一),将 (t*(路程总距离)) 可以得知我们走了的距离。因此,我们去遍历t[0~1],再去乘上总距离,即可算出当前坐标。
  • 众所周知,一条直线可以被拆分为水平方向(x)和垂直方向(y)。若得知我们在这条线段上走了的比例为t,则x = t * (x方向的总距离),y同理。

具体代码如下

方案一
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); 
    } 
}

解释:

拿x举例:

  • (x1-x0)表示x1到x0的距离。
  • t为一个分数,是x0到x1距离比的一个系数。若t为1/10(十分之一)相当于从x0走到x1走了十分之一的距离(注意是距离而不是从x0开始走后的坐标
  • 开头的x0表示从x0开始

嗯(点点头),看起来还不错。

但是如果我们把代码中的系数t改为0.1会怎样呢?结果如图

我们发现图中直线的点变稀疏了。

所以如上代码无法确定常数t为何值可以得到我们想要的一条直线,而且它的效率低下,最理想的绘制直线算法肯定是在一条直线中有几个格子就画几次。

方案二

        如果根据我们走过的比例来计算坐标的方法行不通,那还可以用什么方法绘制一条直线呢?

不多废话,直接上代码。

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,计算出当前在x方向上走的比例t(即走的距离和总距离之比),那不就可以算出y轴方向上走的距离了吗?

这种方法需要绘制多少像素就绘制多少像素,相较于上面第一种方法更加高效。

        我们代入不同的值调用函数。

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

 这里第一个line函数和第三个line函数调用的点坐标相同,只是始终点顺序不同。

结果如下:

似乎我们有遇到了问题:

  1. 我们想要的是连续不间断的线,红色的线不是我们想要的结果。
  2. 那为什么第三个line函数没有覆盖掉第一个line函数的结果呢?

我们先来解决第二个问题,我们会发现,第三个line的x1<x0,它会直接退出函数,因此我们只需添加如下代码即可。

 if (x0 > x1) { 
        std::swap(x0, x1); 
        std::swap(y0, y1); 
    } 

我们的目的是画一条直线,由于算法需要,我们必须要求x0小x1大,所以我们要将两点坐标互换。

第二个问题就轻松解决了,那第一个问题如何解决呢?

要知道怎么解决问题,我们必须知道它是怎么发生的。

这里先补充一些前置知识,

斜率k=dy/dx=tanα

dy||dx = y||x的变化量(就是总长度) 

α = 与x轴正方向形成的夹角

        如图表示我们在屏幕上绘制的像素,白线代表斜率为1的直线,即y=x。红线代表斜率小于1大于0的直线,黄线为斜率大于1的直线。

        我们的第二种算法思路就是根据x算出对应的y,随后x++,计算下一个x对应的y。当我们遇到红线或白线这种情况时,这个算法不会出现什么问题,一个x只需计算一个y值即可。但是当我们遇到黄线这种情况时,这个算法就会出现问题,此时一个x会对应多个y值。

 那我们怎么解决呢?

        如图所示,我们可以先将其放倒(关于y=x作对称)(就是反函数),得到蓝线,此时蓝线斜率0<k<1,再用我们的算法遍历蓝线的x,算出蓝线的y,(y,x)即为黄线的坐标。

        我们上面只讨论了斜率为正的情况,斜率为负值时一个x也会对应多个y,所以我们处理方法相同。

红色白色区域为0<=k<=1的区域,黄色为k<0的区域

void line(int x0, int y0, int x1, int y1, TGAImage &image, TGAColor color) { 
    //斜率大于1时为true
    bool steep = false; 
    //判断斜率是是否>1
    if (std::abs(x0-x1)<std::abs(y0-y1)) { 
        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); 
        } 
    } 
}

我们再次调用上述的三个line函数结果如图。

嗯(再次满意地点点头),这次总没问题了吧。

但是!我又要说但是了。

        有的小伙伴认为最终版本就这水平吗?虽然它的简短且可读性强,但这明显就很低效(执行多次浮点型的除法),并且这个算法并没有检查超界的情况。但是在这篇文章中,我们不会修改这个可行的代码,因为它可读性非常强。同时,我还会系统的进行边界检查(有点像机翻,意思就是之后会检查越界情况)。

        因此我们可以对其进行优化(省略......)

后面的都是优化方案,我们之后都是使用方案二的算法,感兴趣的小伙伴可以继续看下去。不感兴趣的可以直接跳到绘制模型那一段。

方案三

如上述所说,我们要针对以下缺陷进行优化:

  1. 做了多次的除法运算(除法运算效率低)
  2. 做了多次浮点型运算(浮点型计算效率也低)

画线处为需要优化的地方

        我们的画线算法都是从左往右画的。我们的最终目的就是画出一条紧密的直线。所以,随着x的增加(x++),y的变化就只能有三种情况(y+1,y+0,y-1)

那怎么判断当前的y怎么变化呢?可以根据斜率,y=kx+b中,x每增加1,y都会增加k

(k = dy/dx = tanα)。在离散的屏幕上,y轴只有+1,-1和不变这三种情况。所以我们可以使用error term来记录y的累加值,每次error(初值为0)会随着x的+1而增加k,当error>0.5,代表y要进行+1或-1变化,之后应该把error-1。因为一开始我们是在起始点中间(y = y0 + 0.5)处开始记录error值,在y+1之后我们都是在y=0处开始记录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); 
    } 
    //前面的都不变,检查斜率

    //dx,dy表示x,y方向的变化量
    //只要作用就是为了计算derror
    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) { 
            // y1>y0代表要绘制的直线是呈现上升趋势的,所以要y+1
            y += (y1>y0?1:-1); 
            //让error处于[0,1]范围内
            error -= 1.; 
        } 
    } 
} 
方案四

        上述方案其实还是可以优化的

我们使用浮点型的唯一作用就是在循环中使用它来比较是否大于0.5从而控制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)) { 
        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; 
        } 
    } 
} 
 方案五的解释

对于方案五我就不误人子弟了,大家可以去看看这位大佬的讲解。 

这就是Bresenham算法了,它的主要目的就是绘制直线值去除浮点值,使提高效率。

线框渲染

打开课件可以看到obj文件,我们使用obj文件存储模型信息。用记事本之类的软件打开后我们会看到一对模型信息:

v 0.608654 -0.568839 -0.416318

v 代表顶点,后面的三个数为x,y,z坐标信息。

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

f代表面 ,后面跟着三组信息,每组信息第一个数字代表第几个顶点(一个面由三个顶点组成)。后面两个值后面会解释。

#include <vector> 
#include <iostream> 
#include "geometry.h"
#include "tgaimage.h" 
#include "model.h"

const int width = 800;
const int height = 800;

TGAColor white(255, 255, 255, 255);


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

Vec3f light_dir(0, 0, -1);
int main(int argc, char** argv) {
    TGAImage image(width, height, TGAImage::RGB);
    Model* model = new Model("E:\\tiny_renderer_dir\\models\\obj\\african_head.obj");

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

    image.flip_vertically(); // to place the origin in the bottom left corner of the image 
    image.write_tga_file("framebuffer.tga");
    return 0;
}

 

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zmzzz666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值