RaysRenderer:从画一个点到3d软渲染器(1) 从画线开始

因为我们在“重新造轮子”,要从头开始写,没有其他任何函数给我们用,甚至连画线的函数都要自己写。
由于图像是点阵的,我们只能画点,需要用离散的点模拟出线,比如下面这个例子

那么我们应该怎么去画这样的线段呢?
我们知道,两点确定一条直线,表示一条线段的最好方式就是给出2个端点。设2个端点坐标为$(x_a,y_a)$、$(x_b,y_b)$,则我们可以使用两点式表示出改线段所在直线
$$\frac{x-x_a}{x_b-x_a}=\frac{y-y_a}{y_b-y_a}$$
我们可以把该直线写作点斜式:
$$y-y_a=k(x-x_a)$$
其中
$$k=\frac{y_b-y_a}{x_b-x_a}$$
可以写成
$$y=\frac{y_b-y_a}{x_b-x_a}(x-x_a)+y_a$$
因此,我们不妨就遍历所有整数$x_i\in[x_a,x_b]$,找到对应的y,然后四舍五入得到整数$y_i$,那么就可以画出一条线段。
函数实现如下:

void draw_line(int xa, int ya, int xb, int yb, TGAImage &image, TGAColor color)
{
    if(xa > xb) //保证xa<xb
    {
        swap(xa, xb);
        swap(ya, yb);
    }
    //浮点y
    double current_y = ya;
    //步长(也可以理解为斜率)
    double step_y = (yb - ya) / (double)(lenx); 
    for(int xi = xa; xi <= xb; xi++)
    {
        int yi = (current_y + 0.5); //四舍五入
        image.set(xi, yi, color);//画点
        current_y += step_y;
    }
}

我们在main函数里绘制2条线段

int main(int argc, char** argv)
{
    TGAImage image(512, 512, TGAImage::RGB);
    draw_line1(10, 10, 90, 50, image, red);
    draw_line1(10, 10, 90, 170, image, red);
    image.flip_vertically();  //使左下角为原点
    //使用时间作为文件名
    time_t timer;
    time(&timer);
    string com = "./output/" + to_string(timer) + ".tga";
    image.write_tga_file(com.c_str());
    return 0;
}

得到的效果如下图,我们会发现,斜率$k\in[-1,1]$的线段可以很好地画出,但是更陡峭一些的线,如果使用x遍历,那么y每次跳跃超过1,并不连续。

我们考虑优化一下,在$\left|k\right|>1$情况下,我们会发现,我们可以把它看成$k’=\frac{1}{k}$的对称情况。因此,在这种情况下,用y遍历就可以保证线段连续。最终代码如下:

void draw_line(int xa, int ya, int xb, int yb, TGAImage &image, TGAColor color)
{
    int lenx = abs(xa - xb), leny = abs(ya - yb);//在x\y方向上的长度
    if(lenx >= leny) //如果斜率在[-1,1],就以横坐标遍历
    {
        if(lenx == 0) //如果是一个点
        {
            image.set(xa, ya, color);
            return;
        }
        if(xa > xb) //保证xa<xb
        {
            swap(xa, xb);
            swap(ya, yb);
        }
        double current_y = ya; //浮点的y
        double step_y = (yb - ya) / (double)(lenx); //步长(也可以理解为斜率)
        for(int xi = xa; xi <= xb; xi++)
        {
            int yi = (current_y + 0.5); //四舍五入
            image.set(xi, yi, color);
            current_y += step_y;
        }
    }
    else //斜率绝对值大于1,以纵坐标遍历
    {
        if(ya > yb)
        {
            swap(xa, xb);
            swap(ya, yb);
        }
        double current_x = xa;
        double step_x = (xb - xa) / (double)leny;
        for(int yi = ya; yi <= yb; yi++)
        {
            int xi = (current_x + 0.5);
            image.set(xi, yi, color);
            current_x += step_x;
        }
    }
}
/*补充说明:这段代码可以处理一些极端情况。
当线段起点终点重合,进行了特判,避免了除以lenx(==0)的情况。
当斜率不存在或为0时,并不会出错,因为在这种情况下,会分别进入对应横向的遍历方式,避免了除以lenx/leny(==0)的情况。
*/

这样就可以连续地画出所有的线啦!

最后,我对该画线函数进行了性能测试。在512×512的图片上,随机绘制 $10^5$条直线,用时平均约0.27秒,可以看出效率还可以接受。

/*
测试 draw_line() 速度
随机绘制100000条线 
三次分别用时 0.280 s 0.267 s 0.275 s
*/
for(int i = 0; i <= 100000; i++)
{
    draw_line(rand() % 512, rand() % 512, rand() % 512, rand() % 512, image, red);
}

到此为止,我们的渲染器终于可以画线啦,是不是很激动[doge]。开个玩笑,我们忙活了这么久,居然才实现了这么一个简单的功能,后面还有很长的路要走。

来自


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注