目录
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函数调用的点坐标相同,只是始终点顺序不同。
结果如下:
似乎我们有遇到了问题:
- 我们想要的是连续不间断的线,红色的线不是我们想要的结果。
- 那为什么第三个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函数结果如图。
嗯(再次满意地点点头),这次总没问题了吧。
但是!我又要说但是了。
有的小伙伴认为最终版本就这水平吗?虽然它的简短且可读性强,但这明显就很低效(执行多次浮点型的除法),并且这个算法并没有检查超界的情况。但是在这篇文章中,我们不会修改这个可行的代码,因为它可读性非常强。同时,我还会系统的进行边界检查(有点像机翻,意思就是之后会检查越界情况)。
因此我们可以对其进行优化(省略......)
后面的都是优化方案,我们之后都是使用方案二的算法,感兴趣的小伙伴可以继续看下去。不感兴趣的可以直接跳到绘制模型那一段。
方案三
如上述所说,我们要针对以下缺陷进行优化:
- 做了多次的除法运算(除法运算效率低)
- 做了多次浮点型运算(浮点型计算效率也低)
画线处为需要优化的地方
我们的画线算法都是从左往右画的。我们的最终目的就是画出一条紧密的直线。所以,随着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;
}