软件光栅化(Sokolov的教程)一

简介

这段时间在学习Sokolov的教程与OpenGL,Sokolov的教程关于如何写一个光栅渲染器(不借助OpenGL代码),记录下自己的感想

前言

该教程简介:
提醒:这是一份可以轻松重复OpenGL结构的材料,它使一个渲染器。我不想教学如何去写OpenGL程序,我想教学OpenGL是如何工作的,我深信如果不理解工作原理是不会写出高效的3D程序的。

最后的代码只有500行。我的学生需要10-20小时制作这个渲染器。输入:包含多边形信息的文件+纹理图像。输出:程序会生成一幅图像。

目标是最大程度地减少外部的依赖,我为我的学生提供了一堂课去学习TGA文件。TGA是一种支持RGB/RGBA/Black/White的一种图像格式。所以,开始,我们获得一种简单的处理图片的方式。应该注意,除了加载和保存图像外,一开始的功能就是设置一个像素的颜色。

绘制一个点

#include "tgaimage.h"
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor red   = TGAColor(255, 0,   0,   255);
int main(int argc, char** argv) {
        TGAImage image(100, 100, TGAImage::RGB);
        image.set(52, 41, red);
        image.flip_vertically(); // i want to have the origin at the left bottom corner of the image
        image.write_tga_file("output.tga");`
        return 0;
}

这段代码使用了 TGAImage 类来创建一个大小为 100x100 的图像,并使用 set 函数在坐标 (52,41) 处设置颜色为红色。然后使用 flip_vertically 函数将图像上下翻转,使得原点 (0,0) 在图像的左下角。最后使用 write_tga_file 函数将图像保存为 TGA 文件 “output.tga”。后续我会具体学习TGAColor ,如有感想,我会整理成博客。

绘制一条线

我们自己来写代码。应该怎么写一个从点(x0, y0)到点(x1, y1)的线段?
翻译成我们更好理解的方式应该是:通过.set函数与一些数学算法
如何实现一个 line 函数,用于在 TGAImage 类型的图像 image 上绘制从点(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 + (x1-x0)*t; 
        int y = y0 + (y1-y0)*t; 
        image.set(x, y, color); 
    } 
}

1.使用一个循环从 0 到 1 递增 0.01 的步长,每次循环都计算出当前的 t 值。
2.使用当前的 t 值计算出当前点的横坐标 x 和纵坐标 y。
3.使用 image.set 方法在图像上绘制一个像素点。

这个函数的实现方式是使用参数化方程绘制线段,即使用下面的参数化方程计算线段上的每个点的坐标:
x = x0 + (x1 - x0) * t
y = y0 + (y1 - y0) * t
其中 t 是一个从 0 到 1 的参数,用于控制线段的长度。当 t 为 0 时,线段的起点为 (x0, y0);当 t 为 1 时,线段的终点为 (x1, y1)。
这种方法的优点是简单易懂,但是缺点是绘制的线段可能不够平滑,可能存在锯齿状的现象。如果你希望绘制的线段更平滑,可以使用其他的线段绘制算法,例如 DDA 算法、Bresenham 算法等。该教程使用Bresenham 算法
图片可以配合Sokolov的教程使用,该教程非常完善,在此只是阐述想法,方便读者理解。教程地址可以点这

继续绘制一条线

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

这段代码的 line 函数和前一段代码的实现方式略有不同,它使用的是另一种参数化方程绘制线段。

和前一段代码相比,这段代码中的参数化方程计算线段上每个点的纵坐标 y 是这样的:
y = y0 * (1 - t) + y1 * t
其中 t 是一个从 0 到 1 的参数,用于控制线段的长度。当 t 为 0 时,线段的起点为 (x0, y0);当 t 为 1 时,线段的终点为 (x1, y1)。

这个函数的基本流程是:

1.使用一个循环从起点的横坐标 x0 到终点的横坐标 x1 递增 1 的步长,每次循环都计2.算出当前的 x 值。
3.使用当前的 x 值计算出当前点的纵坐标 y。
4.使用 image.set 方法在图像上绘制一个像素点。

这种方法的优点是计算简单,将线段的横坐标作为循环变量。但是缺点(看教程里的图)斜率越大,直线上的“洞”越多,直至消失,存在斜率误差,即当线段斜率较大时,线段可能会看起来不够直甚至消失。

继续绘制一条线 改正斜率问题

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

这段代码的 line 函数和前两段代码的实现方式略有不同,它首先会判断线段的斜率是否较大,如果斜率较大,则会将图像进行转置,然后再使用参数化方程绘制线段。

这个函数的基本流程是:

1.判断线段的斜率是否较大,如果斜率较大,则交换起点和终点的横纵坐标。
2.判断起点的横坐标是否大于终点的横坐标,如果是,则交换起点和终点的横纵坐标。
3.使用一个循环从起点的横坐标 x0 到终点的横坐标 x1 递增 1 的步长,每次循环都计算出当前的 x 值。
4.使用当前的 x 值计算出当前点的纵坐标 y。
5.如果图像被转置了,则使用 image.set 方法在转置后的图像上绘制一个像素点;否则,在原图像上绘制一个像素点。
这种方法的优点是可以有效地避免斜率误差,即使线段斜率较大,也能绘制出较平滑的线段。但是缺点是实现较为复杂,需要对图像进行转置。
其实就是优化了斜率较大的时候的绘制方法。

继续绘制一条线 Bresenham 画线算法

复制颜色话费了10%的时间,70%的时间花费在了绘制直线上!这正是咱们需要优化的地方。每个除法都有相同的除数。让我们把它从循环里面拿出来。误差变量给出我们当前点(x, y)到直线的距离,每次误差大于一个像素的时候,我们将y增加或减小1,当然误差也需要增加减小1.

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

Bresenham 算法是一种基于整数计算的线段绘制算法,它能够快速绘制精确的线段。Bresenham 算法的基本流程是:

判断线段的斜率是否较大,如果斜率较大,则交换起点和终点的横纵坐标。
判断起点的横坐标是否大于终点的横坐标,如果是,则交换起点和终点的横纵坐标。
计算线段的横向距离 dx 和纵向距离 dy。
计算误差增量 derror。
使用一个循环从起点的横坐标 x0 到终点的横坐标 x1 递增 1 的步长,每次循环都计算出当前的 x 值。
计算当前点的纵坐标 y。
如果图像被转置了,则使用 image.set 方法在转置后的图像上绘制一个像素点;否则,在原图像上绘制一个像素点。
根据当前点的误差值 error 更新下一个点的纵坐标。如果误差值大于 0.5,则将纵坐标加上 (y1>y0?1:-1),并将误差值减去 1。
Bresenham 算法的优点是计算简单,速度快,能够绘制出精确的线段。但是缺点是无法绘制斜率较大的线段。
Bresenham 算法的优点
1,不必计算直线之斜率,因此不做除法(除法比较慢);
2,不用浮点数,只用整数;
3,只做整数加减法和乘2运算,而乘2运算可以用硬件移位实现.

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值