[TinyRenderer] Lesson 1 布兰森汉姆绘制线算法
文章目录
- 翻译
- 1 第一次尝试
- 2 第二次尝试
- 3 第三次尝试
- 4 第四次尝试
- 5 第四次尝试+
- 6 第五次和最后一次尝试
- 7 线框渲染
- 实操效果
翻译
布兰森汉姆绘制线算法
1 第一次尝试
第一课的目标是渲染金属丝网。要做到这一点,我们应该学习如何绘制线段。我们可以简单地读懂Bresenham的行算法,但是让我们自己写代码。在(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); }
}
可用代码片段here
2 第二次尝试
这段代码的问题(除了效率低下)是选择常数,我取它等于.01。
如果取它为.1,线段就会像这样
我们可以很容易地找到必要的步骤:它就是要绘制的像素数。
最简单的(但错误的)代码看起来像以下:
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-x0) '。
然后,如果我们尝试用这段代码绘制以下线条:
line(13, 20, 80, 40, image, white);
line(20, 13, 40, 80, image, red);
line(80, 40, 13, 20, image, red);
原来一条线是好的,第二条是有孔的,第三条线根本就没有。
注意,第一行和第三行(在代码中)以不同的颜色和不同的方向绘制了同一条线(源点和目标点翻转了)。
我们已经看过白色的了,画得很好。
我希望把白线的颜色改成红色,但是我做不到。
这是对对称性的测试:绘制线段的结果不应该依赖于点的顺序:(a,b)线段应该与(b,a)线段完全相同。
3 第三次尝试
我们通过交换点来修复缺失的红线,使x0总是小于x1。
在其中一个线段上有洞,这是由于它的高度大于宽度。
我的学生经常提出以下解决办法:
if (dx>dy) {for (int x)} else {for (int 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)) { // 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); } }
}
4 第四次尝试
_警告:编译器的优化器(g++ -O3)往往比你(和我)在创建一个快速代码。这一部分是出于历史/文化原因。
这段代码效果很好。这正是我希望在最终版本或我们的渲染器中看到的那种复杂性。它绝对是低效的(多分区等),但它简短易读。请注意,它没有声明,也没有检查越界,这很糟糕。在这些文章中,我尽量不重载这个特定的代码,因为它会被大量阅读。同时,我系统地提醒进行检查的必要性。
所以,前面的代码工作正常,但我们可以优化它。优化是一件危险的事情。我们应该清楚代码将在哪个平台上运行。为显卡或仅为 CPU 优化代码是完全不同的事情。在任何优化之前和期间,必须对代码进行概要分析。试着猜一下,这里哪个操作是最耗费资源的操作?
对于测试,我绘制了我们之前绘制的 3 条线段 1,000,000 次。我的 CPU 是 Intel® Core™ i5-3450 CPU @ 3.10GHz。对于每个像素,此代码调用 TGAColor 复制构造函数。即 1000000 * 3 条线段 * 每条线段大约 50 个像素。引起了很多问题,不是吗?从哪里开始优化?分析器会告诉我们。
I compiled the code with g++ -ggdb -g -pg -O0 keys, and then ran gprof:
我用g++ -ggdb -g -pg -O0
命令编译代码,然后运行 gprof:
% cumulative self self total time seconds seconds calls ms/call ms/call name 69.16 2.95 2.95 3000000 0.00 0.00 line(int, int, int, int, TGAImage&, TGAColor) 19.46 3.78 0.83 204000000 0.00 0.00 TGAImage::set(int, int, TGAColor) 8.91 4.16 0.38 207000000 0.00 0.00 TGAColor::TGAColor(TGAColor const&) 1.64 4.23 0.07 2 35.04 35.04 TGAColor::TGAColor(unsigned char, unsigned char, unsigned char, unsigned char) 0.94 4.27 0.04 TGAImage::get(int, int)
10% 的时间花在复制颜色上。 但是随后 70% 的时间都花在了调用 line() 上! 这就是我们要优化的地方。
5 第四次尝试+
我们应该注意到每个部门都有相同的除数。 让我们把它排除在外。 误差变量为我们提供了从当前 (x, y) 像素到最佳直线的距离。 每次误差大于一个像素时,我们将 y 增加(或减少)1,并将误差也减少 1。
可用代码here.
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.; } }
}
这是 gprof 的输出:
% cumulative self self total time seconds seconds calls ms/call ms/call name 38.79 0.93 0.93 3000000 0.00 0.00 line(int, int, int, int, TGAImage&, TGAColor) 37.54 1.83 0.90 204000000 0.00 0.00 TGAImage::set(int, int, TGAColor) 19.60 2.30 0.47 204000000 0.00 0.00 TGAColor::TGAColor(int, int) 2.09 2.35 0.05 2 25.03 25.03 TGAColor::TGAColor(unsigned char, unsigned char, unsigned char, unsigned char) 1.25 2.38 0.03 TGAImage::get(int, int)
6 第五次和最后一次尝试
为什么我们需要浮点数? 唯一的原因是 dx 除以 dx 并与循环体中的 .5 进行比较。 我们可以通过用另一个变量替换错误变量来摆脱浮点数。 我们称之为error2,假设它等于error * dx * 2。这是等效的代码:
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; } }
}
% cumulative self self total time seconds seconds calls ms/call ms/call name 42.77 0.91 0.91 204000000 0.00 0.00 TGAImage::set(int, int, TGAColor) 30.08 1.55 0.64 3000000 0.00 0.00 line(int, int, int, int, TGAImage&, TGAColor) 21.62 2.01 0.46 204000000 0.00 0.00 TGAColor::TGAColor(int, int) 1.88 2.05 0.04 2 20.02 20.02 TGAColor::TGAColor(unsigned char, unsigned char, unsigned char, unsigned char)
现在,通过引用传递颜色(或仅启用编译标志 -O3),在函数调用期间删除不必要的副本就足够了,就大功告成了。 代码中没有一个乘法或除法。 执行时间从 2.95 秒减少到 0.64 秒。
我还建议检查 this issue。 优化很棘手!
7 线框渲染
所以现在我们准备好创建一个线渲染。 你可以在这里找到代码和测试模型的快照。
我使用文件的 wavefront obj 格式来存储模型。 我们渲染所需的全部内容是从文件中读取以下类型的顶点数组:
v 0.608654 -0.568839 -0.416318
是 x,y,z 坐标,每个文件行一个顶点和面孔
f 1193/1240/1193 1180/1227/1180 1179/1226/1179
我们对每个空格后的第一个数字感兴趣。 它是我们之前读过的数组中顶点的编号。 因此,这条线表示 1193、1180 和 1179 个顶点形成一个三角形。 请注意,在 obj 文件中,索引从 1 开始,这意味着您应该分别查找 1192、1179 和 1178 个顶点。
model.cpp 文件包含一个简单的解析器。 将以下循环写入我们的 main.cpp 和 voilà,我们的线渲染器已准备就绪。
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); }
}
下次我们将绘制 2D 三角形并改进我们的渲染器。
实操效果
[TinyRenderer] Lesson 1 布兰森汉姆绘制线算法相关推荐
- 布兰森汉姆画圆matlab,bresenham算法画直线
实验一名称:基本图形的生成算法 要求:(1)掌握 DDA 生成线段算法 (2)掌握 Bresenham 生成线段算法 (3)掌握生成圆弧算法 1. 代码 (1) Bresenham 画线算法 v 实验 ...
- Bresenham’s algorithm( 布兰森汉姆算法)画直线
简介 1967年,IBM的J.Bresenham提出了Bresenham算法. Bresenham算法是在一些约定条件下的最佳逼近. Bresenham算法通过前一个像素点提供的信息来 ...
- 布雷森汉姆直线演算法(Bresenham‘s line algorithm)介绍
布雷森汉姆直线演算法(Bresenham's line algorithm)是用来描述两点间决定一条直线的算法,本人发现它可以用于确定栅格地图中两点间直线经过的栅格位置,它会算出一条线段在点阵图上最接 ...
- 在yuv上画线_利用布雷森汉姆算法绘制在YUV图像上画直线
最近,因工作需要,在YUV图像上画直线: 算法1步骤: 1.已知直线的起点和终点: 2. 利用布雷森汉姆算法在两点间画直线: 3. 将该直线上的点的颜色在YUV图像上画出. 布雷森汉姆算法原理参考wi ...
- 易懂的Bresenham 布雷森汉姆算法画圆的原理与Python编程实现教程
Bresenham 布雷森汉姆算法画圆的原理与编程实现教程 注意:Bresenham的圆算法只是中点画圆算法的优化版本.区别在于Bresenham的算法只使用整数算术,而中点画圆法仍需要浮点数.注意: ...
- Python布雷森汉姆直线算法RViz可视化ROS激光占位网格映射
使用对数赔率映射已知姿势算法(ROS 包). 布雷森汉姆直线算法 布雷森汉姆直线算法是一种线绘制算法,它确定应选择的 n 维栅格的点,以便形成两点之间的直线的近似值. 它通常用于在位图图像中(例如在计 ...
- 【附源码】布雷森汉姆直线算法(bresenham‘s line algorithm)
1.布雷森汉姆直线算法简介 我们在纸上画直线时,只需要定一个起点和终点,然后把两点连接起来就是一条直线,你将会得到一条笔直的直线. 但是,这个简单的过程,在计算机上却并不容易.首先计算机的屏幕是一个一 ...
- ITK:布雷森汉姆线BresenhamLine
ITK:布雷森汉姆线BresenhamLine 内容提要 输出结果 C++实现代码 内容提要 在两点之间的布雷森纳姆线上获得点. 输出结果 [0, 0] [1, 1] [2, 2] [3, 3] [0 ...
- 森汉姆算法(AS3)-检测两点之间所经过的节点
为了把A*寻到的路径中一些没必要的节点去掉,可以用到森汉姆算法,如果两点之间经过的节点都是可走的,那就把这两个点连起来,最终拿到的路径就是比较平滑的了. 下面是算法实现: ? public stati ...
最新文章
- oracle创建表+注释
- linux 魔兽 qq
- UOJ37. 【清华集训2014】主旋律
- 解决umount.nfs: /data: device is busy 问题
- java并发 设计模式,Java并发——设计模式
- HashTable数据类型的增删改查操作0404
- python多重继承_Python多重继承
- ios xcode如何在控制台打印frame
- 线程 -- ThreadLocal
- MATLAB 图像处理基础(2)
- 汉字笔画动图怎么做_怎么用flash制作汉字笔顺的动画
- 怎么做到对mac电脑的监控呢?
- C#批量转换Word文档为Pdf
- 八款优秀的 Linux 轻量级 Web 浏览器
- U-Boot中Distro_bootcmd的实现分析
- 【python】使用in判断元素是否在列表(list)中,如何提升搜索效率?
- 英雄联盟——心得体会
- 计算机启动修复怎么还原,电脑开机出现系统恢复选项该怎么办
- linux 下动手实现bash -lR 命令
- java signal信号_Java Signal实例