软件光栅化(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运算可以用硬件移位实现.