从零开始实现3D软光栅渲染器 (3) 绘制直线

简介

上一节中我们在canvas中绘制了点,这一节我们来绘制直线。

计算机图形学中,绘制直线的算法很多,比如:DDA算法,中点画线算法…

今天我们来讲一个经典的算法:Bresenham算法。经典之所以是经典,因为它既保证了绘制直线的效率,而且也能绘制圆弧、抛物线等。

大家都知道,显示器屏幕像素是由像素组成的,我们看到画面的过程,其实就是每个像素填充不同的颜色罢了。简单说,不就是一个二维数组嘛,只不过数组中每个元素存储的是颜色值而已。我们上一节绘制点,就是在canvas中对应的图像缓冲区中填颜色嘛,而canvas的图像缓冲区屏幕的显示原理一样嘛,也是个二维数组嘛。我们在canvas上绘图,就等于把canvas当作显示器嘛。

如果只能显示静态的图像,那你可能买了个假显示器,最起码也得能够放个动画片呀。所以,显示器一般都是不停刷新的,专业点又叫扫描。至于为什么要扫描,这个和显示器的显示原理有关系,我们就不深究了。一般来说,是先扫第一行,再扫第二行,再扫第三行… 当扫到最后一行,你就看到一幅完整的图像了。当这个扫面速度够快的话,由于人眼特殊的成像物理结构,你就会看到流畅的画面。这个扫完一次,我们叫做一帧。我们玩游戏的时候,经常会看到帧率(FPS),就是一秒钟能够扫多少次,扫30次,那就是30帧。

但是你想想,什么叫足够快?1秒种扫描30次?60次?120次?一般来说,一秒扫30次,也就是30帧的时候,我们肉眼就能看到流畅的画面了,低于30帧那就影响你的游戏体验了。

在这里插一句,我们玩游戏的时候,经常会看到有个【绘制同步】选项。到底勾选不勾选呢?选不选有啥影响呢?

这个其实就是和显示器的显示原理相关的。我们想想,游戏的画面是不是实时绘制到屏幕上的,实时哦。你放一个Q,屏幕上立马就能看到Q的特效,还能看到你这个Q打中敌人没有,如果打中了敌人会有什么反应,敌人有没有护甲,减多少血,他血够不够,他队友有没有奶妈,奶妈是不是毒奶。。。所有的这些你都是实时看到的哦。所以,游戏不仅要实时显示画面,还要实时计算这些游戏逻辑。这些都是要时间的呀。我们的显示器刷新也是要时间的呀。显示器扫描一次(显示完一帧),就会发一个同步信号,看游戏有没有把画面传过来。如果游戏此时还没有把画面准备好(因为它又要渲染,又要计算逻辑,它来不及呀),那你显示器是等它呢,还是不等?

如果等它,那你就勾上垂直同步,结果就是,等游戏运算完毕,显示器开始接着扫描,用户就能看到完整的图像,缺点就是,卡呀。你得等半天呀,画面就是那么一顿—一顿___。特别的配置差的电脑,CPU处理不了那么多数据,显示器老是死等游戏快点算完,给我画面,我来显示,那不是每个用户都能等得了的。比如说我就等不了,因为小伙子我火气大,玩个游戏还得受气???

如果显示器不等呢?那就不勾选,那显示器扫完一帧,不管游戏下一帧的数据有没有计算好,都接着扫。那效果是什么?那就可能出现画面撕裂的现象。因为下一帧数据还没准备好,你就急着显示,谁知道你会显示成什么样,是吧。

那么问题来了?我们什么时候勾选,什么时候不勾选?

  1. 我电脑不是太好,勉强能带的动这个游戏。那就不勾选垂直同步。因为此时显示器的扫描速度略大于你游戏运算的速度,不勾的话,也就是显示器不等游戏运行完就扫下一帧,反正两者的速度差不多,画面会有轻微的撕裂,影响也不是很大,用户的体验相对来说好点。如果勾选的话,反而会有卡顿的感觉。你仔细品下。
  2. 我电脑顶配,这个游戏,我能同时开3个。。。这么好的电脑玩游戏,你会打开这个面板?你会烦恼哪个配置开不开?你跟我说说,上面那个面板中哪个设置你不是想开就开???你会在乎 垂直同步开不开??? 只有打游戏时,电脑卡卡的,或者画面不稳定的,才会打开设置面板,还念念有词,哎呀,是不是我哪个配置没开呀,或者哪个没关呀。 当然,你如果想知道上面那些设置开和不开背后涉及到哪些图形学原理,咱们以后再叨。。。

好像是这么回事。我瞎说的。

好像扯远了。没办法一拿起键盘,就文思泉涌。

Bresenham直线绘制算法

数学上的直线定义在全体实数区间,拿 y=kx+b 来说,x可以取全体实数。可你的显示器是“马赛克”的呀,你只能取整数呀。直线光栅化算法,就是确定屏幕上哪些像素连起来更接近数学上的一条直线。其中,Bresenham算法是其中做的较好的一个。

我们来看Bresenham算法是怎么确定要绘制的像素点的。

直线的斜截式方程:y = k * x + b

斜率 = k ,也就说 x 每加1,y就加k。

y1 = k * (x + 1) + b = k * x + b + k = y + k

现在我们过显示器每个像素的中心,连接一个二维的网格。那么绘制直线的问题,就变成:在横轴x步进的过程中,y选择哪个节点像素的问题。

我们设置一个误差变量d(表示直线与网格的交点与像素的距离),通过判断这个d值来决定选择上方的像素还是下方的像素。

一开始,d0 = 0.

当 x 步进1个像素,则直线与网格有个焦点Q,此时Q的相对高度为k(因为x步进1,y步进k嘛),此时d=k,那么如果这个d在中点下面,很明显,y就取pd,也就是说x步进1,直线的下个像素取右边的一个像素。很明显,图中画的就是这种情况。

那么x接着步进1,此时d有可能超过中点(也有可能没超过,我们按照图中画的情况来说),很明显,此时应该取上面的那个像素。

这里我们要记住的是,一旦在y方向步进了1(也就是d>0.5时),就要立即更新d=d-1,否则你下次迭代的时候d=d+k(此时d就超过1了),因为d只是记录误差的变量,是不能超过1的,这个很容易理解吧。

那么,可以总结为:

这里的0.5就是前面说的那个中点,因为上下两个像素的间隔1个像素嘛,一半就是0.5个像素嘛。当然这个就是相对的概念,就相当于我们小学老师说的那个“单位1”的概念差不多的。

简单说,就是x步进1,y变不变,就看d的值,d大于0.5,y就步进1,否则不变。

其中,每次步进,d=d+k,d的初始值为0.

下面看下代码:

是不是很简单?此时给你起点P1(x1,y1)和终点P2(x2,y2)是不是就可以画处一条直线来了?那可不。肯定可以啊。

优化

咱们再想想,CPU做整数加法快?还是小数加法快?当然是整数加法快。因此我们希望将误差d的计算变为整数加法。别看只有这点提升,你想想,一个游戏角色有多少条直线构成的?一秒钟要绘制多少帧画面?量变引起质变!!!

那么怎么变小数加法为整数加法呢?

骚操作1

令 e = d - 0.5,说白了就是用d-0.5替换之前的d,起个绰号叫e。

那么之前的公式变为:

也就是说,e>0,y方向加1;e<=0,y方向不变;e的初始值为-0.5。

此时,之前的

就变成

没问题吧。

问题1:你发现没有,e的初始值还是小数,还是没有变成整数呀。

问题2:x每步进1,e=e+k, 这个k怎么求的?

这是斜率k的求法,这个大家应该知道哈。

k = delta(y) / delta(x) = (y2-y1) / (x2-x1)

是不是有个除法?计算机做除法快?还是乘法快?当然乘法快。

所以,我们有什么办法能同时解决上面2个问题。

骚操作2

用 q = e * 2 * delta(x) 来替换e。

那么q的初始值就是:q = -0.5 * 2 * delta(x) = -delta(x)

其中,-0.5就是e的初始值嘛。这很好理解。

e的取值范围是(-0.5,0.5),则q的取值范围为(-delta(x),delta(x))

那么,之前的e=e+k变成什么了?

因为 q = e * 2 * delta(x),则 e = q / (2*delta(x))

e = e + k 可以写成:

e = q / (2 * delta(x)) + delta(y) / delta(x)

两边同时乘以 e * delta(x) 得到:

e * 2 * delta(x) = q + 2 * delta(y)

也就是:

q = q + 2 * delta(y)

你看,是不是既解决了小数加法的问题,又把除法转为乘法了。

简直是太巧妙了!!!

最后,我们总结下算法的步骤:

  1. 输入直线2个端点P1(x1,y1)、P2(x2,y2)
  2. 计算 delta(x) = x2 - x1; delta(y) = y2 - y1; q = -delta(x); x = x1, y = y1
  3. 绘制点 (x,y)
  4. 接着 q = q + 2 * delta(y),判断q的符号,如果q>0,则取(x,y)更新为(x+1,y+1),同时更新 q = q - 2 * delta(x),否则 更新(x,y)为(x+1,y)

注意,我们推导的是 0 < k < 1的直线,且是从左向右画的直线。其他情况,以此类推。源码里有完整的实现,可画任意直线,从任意方向画,没严格测试,胡乱画了几条线,好像没啥问题。

源码:https://www.lanzous.com/ib12isf

欢迎大家关注我的公众号【OpenGL编程】,定期分享OpenGL相关的3D编程教程、算法、小项目。欢迎大家一起交流。

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章