contributed by < sammer1107
>
進階電腦系統理論與實作
這個 class 負責根據一個 Game 物件的狀態來算繪遊戲場景。Renderer 內固定有一個 RayCaster,讓 Renderer 可以使用他來計算場景。
這個方法從一個亮度值來創造一個 ARGB 像素,ARGB 的值單純就只是複製亮度 brightness
而已。而 A,R,G 和 B 各用 8 bit 來表示,一起放在一個 32 bit 整數內,所以可以用 shift operator <<
來設定。
這個方法根據傳遞的 Game 物件,會利用 RayCaster 計算場景,然後畫到 frameBuffer 上。
這裡 frameBuffer 是一個以一維型式儲存的二維陣列,大小為 SCREEN_WIDTH * SCREEN_HEIGHT
。儲存方式為 row major。
首先開始算繪場景前,我們先將 RayCaster 初始化在玩家目前位置。這裡將 x,y 座標乘以 256 是為了轉成 fixed point 格式,如果 RayCaster 實作為浮點數版本,則會在 Start 內將格式還原為 floating point。
再來我們將掃過所有的 x ,在每個 x 把整條垂直線畫出來。再這之前,我們先看 Trace 方法回傳的變數們,這些變數提供我們畫出畫面中某條垂直線所需要的資訊:
2*sso
。牆的高度是可能會比畫面還高的,例如圖的右邊的部份。故我們有這一段 code:
如果牆比畫面高,則設定 sso 為 HORIZON_HEIGHT,ws 則為畫面頂部到牆頂部的距離。牆面的 texture 定義在 raycaster_data.h 中的 g_texture8,為 的亮度矩陣。
tso=0
。否則在牆不能容入畫面時,如圖中右邊, tso 必須從中間某個值開始。有了這些資訊,我們就可以畫出一條垂直線了。
>> 10
的動作。ty*64 + tx
並且用 <<
取代乘法。在每個 x 上畫完垂直線後,就完成整個畫面的繪製了。
這個函數接收 rayX 與 rayY ,然後到地圖中查找這個位置是不是牆壁。
首先我們要先了解地圖與座標系統的運作,觀察此函式與 g_map 定義之後,我得出以下推論:
MAP_X
與 MAP_Y
的定義,這個地圖為 大小。tileX & 0x7
為 0,我們會作 g_map[...] & (1 << 8)
,如此並不會抓到我們想要的 MSB。故 8 應改成 7。tileX >= MAP_X - 1
與 fixed point 實作不一致。若 tileX 為 31 ,我們仍應該到地圖查找而不是直接回傳,所以這裡應該改成 tile > MAP_X - 1
。這個方法根據 playerX,playerY 與 rayA 來發射出光線,並回傳碰撞方向與位置,回傳資訊有兩種可能:
Distance | 碰撞方向(hitDirection) | hitoffset |
---|---|---|
碰撞面垂直 x 軸 (1) | 碰撞點 y 值 | |
碰撞面垂直 y 軸 (0) | 碰撞點 x 值 |
Distance | 特殊情況 |
---|---|
因為浮點數計算誤差 導致撞不到方塊 |
地圖座標系統
在進入 Distance 的細節前,首先我們要了解 x, y, angle 的座標系統。 要注意的是這裡的角度從 y 軸出發,順時針繞一圈。
程式一開始先做了一些前置準備:
再來則計算此光線發射出去會最先碰到的垂直線(平行 y 軸)與水平線(平行 x 軸)。到達水平面要走的 x 距離為 startDeltaX,到達垂直面要走的 y 則為startDeltaY。
再來我們可以看到每個紫色點之間的距離都是固定的,粉色也一樣。我們把紫色點之間的 x 差叫作 stepX,粉色之間的 y 差叫作 StepY。如下圖:
這部份對應的程式碼如下。interceptX 對應到紫色點的 x 座標,interceptY 對應到粉色點的 y。
到這裡已經做好了讓光線開始前進的準備。這部份的 code 長這樣。
somethingDone == true
)的話,就繼續做。至於 somethingDone
存在的意義其實是為了處理浮點數運算的誤差,到後文會有更詳細的解說。verticalHit = true
,準備離開整個迴圈,我們已經找到牆壁了。如此就完成 Distance 了。
如果我們從任何一方格出發,則我們不是先遇到垂直線,就是先遇到水平線,不可能兩個都遇不到。故從邏輯上, 不可能會出現 somethingDone=false
跳出迴圈的情況。但在下面兩種情況,真的有可能會發生這樣的情況。
我一開始觀察到如果將人物生成在 x,y 皆為整數的地方(0~31 都可以被浮點數 exact 表示),則會出現 個世界全部消失的現象。如果只有其中一個是整數,則有半個世界會消失。
回到上面 startDelta 的計算:
我們對 offset=0 也就是人物在整數上面都有做特別的處理,我們會忽略現在身在的邊界,而去尋找下一個邊界當作第一個碰撞點。但這會造成問題。以下圖為例:
現在 interceptX < tileX ,且 interceptY < tileY,就造成了 somethingDone = false
。
解決辦法就是不要偷吃步,從當前邊界做起:
如此 interceptY > tileY,就會進去第一個迴圈而不會 somethingDone = false
了。這在程式碼中只要把原本對 offset == 0 的額外處理拿掉就可以了:
somethingDone = false
的情況。這是由於浮點數的誤差造成,當 interceptX 或 interceptY 很靠近整數時,可能會發生明明會在方格內碰撞,卻跳到方格外一點的情況,造成兩邊條件都不成立。somethingDone = false
,我才在 interceptX 與 interceptY 來加入一個補償值,讓判斷能夠正常。
如此就不會發生因為誤差而 distance = 0 的情況了,雖然影響不大。但是在邊界上,還是有可能 distance = 0。這個方法根據先前 start 得到的 playerX, playerY, playerA (角度),從 screenX 這個位置射出 ray,來得到要畫的 牆壁高度、texture 亮度與 texture 座標。
modff(hitOffset, &dum)
,得到座標的小數部份64 / txs
。而這裡因為要轉成 10 bit fixed point,所以多乘了 變成 256 / txs * 256
。
要從牆壁的高度計算畫面上的高度,我們要先知道垂直方向的 。假設在角度 在某距離可以看到寬度 的範圍且在角度 下可以看到高度 。而我們的 。
所以我們若令中線距離為 則
再來假設牆面距離玩家 ,則透過這個 我們可以看到的遊戲場景一半高就是 (在遊戲中的座標),而牆的一半高度是 0.5。若我們令半牆的 pixel 高度為 則牆佔畫面的比例為
所以把 用上面的式子取代之後得到 。
所以假如要讓 INV_FACTOR 隨著 FOV 自動調整的話,需要改為這樣:
實測 floating point 不一樣的 FOV:
左邊仍為舊的 fixed point (90 度 FOV)。
原本的程式碼中這一段
screenY
為 8bit unsigned,而且當我們很靠近牆時,算出來的高度可能是畫面半高(128)的好幾倍,所以可能會遇到 overflow 的問題。解決辦法是先用 float txs
儲存結果,等判斷完範圍之後,再設定 screenY
:
當走近牆面的時候,會發現定點數與浮點數的差異。
牆面上半部:
牆面下半部:
走到特定點的時候,會出現奇怪的東西。我想是因為距離太遠有東西 overflow 的緣故。我去追查定點數實作中跟距離有關的部份,找到查表的函式 LookupHeight:
ds >= 256
,進入 if 查表後,出來竟然又查了一次,而且存取到 array 長度之外的位置。所以我們只要把 else 正確的補上即可: