從零開始實現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編程教程、算法、小項目。歡迎大家一起交流。

在這裏插入圖片描述

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