# 2020q3 Homework5 (render) contribute by <`simpson0114`> ###### tags: `sysprog2020` ---- ## 作業要求 - [ ] 修正浮點數和定點數算繪程式展現的缺失,並提出改進 precision 及 accuracy 的方式 - [ ] 輸出算繪過程的 frame rate,日後當我們進一步提升算繪程式的效率時,這會是效能評比的方式之一 - [ ] 利用 tools/precalculator.cpp 產生運算表格,修改相關程式碼,使得程式碼在編譯時期才去產生運算表格,後者以標頭檔案 (generated header) 的形式存在並編譯進入主程式 - [ ] 解說現有 fixed-point 實作機制,並探討前述表格產生的機制,需要提及其中的考量點 - [ ] 將 raycaster 以 C99/C11 (或 gnu99/gnu11) 重寫,允許在執行時期載入 fixed-point 和 floating-point 為基礎的 renderer ## 程式執行 進入資料夾 make 後,輸入 `./main` 即可執行程式 ## 程式原理 ### Raycasting raycasting 的原理是在 screen 範圍內打出一條條的光線,偵測物體距離,藉此把 2D 轉為 3D ![reference link](https://upload.wikimedia.org/wikipedia/commons/e/e7/Simple_raycasting_with_fisheye_correction.gif) 在專案內 renderer.cpp 中的 TraceFrame 實現此原理 用 x 去模擬每一條光線 ```c=11 for (int x = 0; x < SCREEN_WIDTH; x++) ``` ## 修正程式缺失 ### floating-point 靠近牆壁畫面破碎 ![](https://i.imgur.com/1iDzn2M.png) 原程式碼 `raycaster_float.cpp`:150 ```c=150 if (distance > 0) { *screenY = INV_FACTOR / distance; auto txs = (*screenY * 2.0f); if (txs != 0) { *textureStep = (256 / txs) * 256; if (txs > SCREEN_HEIGHT) { auto wallHeight = (txs - SCREEN_HEIGHT) / 2; *textureY = wallHeight * (256 / txs) * 256; } } } else { *screenY = 0; } ``` 可觀察到因為 `*screenY` type 為 `uint8_t` ,所以 `*screenY` 會 overflow ,因此我先修正了 overflow 的問題。 又由 `renderer.cpp`:22 ```c=22 nt16_t ws = HORIZON_HEIGHT - sso; if (ws < 0) { ws = 0; sso = HORIZON_HEIGHT; } ``` 因在 `renderer.cpp` 中 `sso` 就是 `*screenY` ,可觀察到若 `sso` > `HORIZON_HIGHT` ,則 `sso` = `HORIZON_HIGHT` ,因此若 `*screenY` overflow ,則將 `*screenY` 設為 `HORIZON_HIGHT` 以下為修改後程式碼 `raycaster_float.cpp`:136 ```c=136 if (distance > 0) { float tmp = INV_FACTOR / distance; *screenY = tmp; auto txs = (tmp * 2.0f); if (txs != 0) { *textureStep = (256 / txs) * 256; if (txs > SCREEN_HEIGHT) { auto wallHeight = (txs - SCREEN_HEIGHT) / 2; *textureY = wallHeight * (256 / txs) * 256; *screenY = HORIZON_HEIGHT; } } } else { *screenY = 0; } ``` 修正後: ![reference link](https://i.imgur.com/H6AltO5.png) ### floating-point 角落牆面消失 ![reference link](https://i.imgur.com/Wu2Gmla.png) 因為在角落才會出現這個 bug ,所以推斷是 `distance` 在邊界時才會發生。 測試後發現是在 `playerX` 或 `playerX` 有趨近 1.0 的情況時就會發生牆面消失,反之則不會。 參考影片 [Wolfenstein 3D's map renderer](https://www.youtube.com/watch?v=eOCQfxRQ2pY) 去釐清原理。 在 raycaster_float.cpp 中測試,經過實驗發現在 `offsetX` 或 `offsetY` 等於 0 時會發生此情形。影片中 $xIntercept = x + dx + -dy / \tan(\theta)$ 對應到程式內的 `float interceptX = rayX + startDeltaX;`。而 `offset` 是影響到 `startDelta` 的關鍵,所以在決定 `startDeltaX` 和 `startDeltaY` 的值時出問題。 這邊看到程式在 offset 為 0 時會特別處理 ```cpp=56 if (offsetY == 0) { startDeltaX = (1) * fabs(tan(rayA)); } else { startDeltaX = (offsetY) *fabs(tan(rayA)); } ``` 我認為這樣很奇怪,按照式子來看 $-dy / \tan(\theta)$ 應該為 0,這邊卻是 $(1) * fabs(\tan(\theta))$。所以對式子進行修正,將 1 改為 0 ```cpp=56 if (offsetY == 0) { startDeltaX = (0) * fabs(tan(rayA)); } else { startDeltaX = (offsetY) *fabs(tan(rayA)); } ``` 發現原本的問題解決了,於是接著精簡程式碼。當 if 成立時 offset 必定是 0,所以整個判斷式變得多餘,直接保留 else 內部的運算就好。 以下為修正的程式碼 ```c=52 float startDeltaX, startDeltaY; if (rayA <= M_PI_2) { startDeltaX = (1 - offsetY) * tan(rayA); startDeltaY = (1 - offsetX) / tan(rayA); } else if (rayA <= M_PI) { startDeltaX = (offsetY) *fabs(tan(rayA)); startDeltaY = -(1 - offsetX) / fabs(tan(rayA)); } else if (rayA < 3 * M_PI_2) { startDeltaX = -(offsetY) *fabs(tan(rayA)); startDeltaY = -(offsetX) / fabs(tan(rayA)); } else { startDeltaX = -(1 - offsetY) * fabs(tan(rayA)); startDeltaY = (offsetX) / fabs(tan(rayA)); } ``` 修正後: ![reference link](https://i.imgur.com/h47KnXS.png) ### fixed-point 角落多一面牆面 ![reference link](https://i.imgur.com/RBowOh3.png) 因為也是在角落才會出現的問題,所以判斷是 `distance` 在邊界才會發生。 觀察 `raycaster_fixed.cpp` 處理 fixed-point 的計算,一樣在程式內尋找使用 distance 的地方。 `raycaster_fixed.cpp`:74 ```c=74 if (distance >= 256) { const uint16_t ds = distance >> 3; if (ds >= 256) { *height = LOOKUP8(g_farHeight, 255) - 1; *step = LOOKUP16(g_farStep, 255); } *height = LOOKUP8(g_farHeight, ds); *step = LOOKUP16(g_farStep, ds); } else { *height = LOOKUP8(g_nearHeight, distance); *step = LOOKUP16(g_nearStep, distance); } ``` 可觀察到在 76 行,當 `ds` 超過 256 時會將 `*height` 和 `*step` 修正,但隨後會再把修正覆蓋掉。 修正後程式: `raycaster_fixed.cpp`:74 ```c=74 if (distance >= 256) { const uint16_t ds = distance >> 3; if (ds >= 256) { *height = LOOKUP8(g_farHeight, 255) - 1; *step = LOOKUP16(g_farStep, 255); } else { *height = LOOKUP8(g_farHeight, ds); *step = LOOKUP16(g_farStep, ds); } } else { *height = LOOKUP8(g_nearHeight, distance); *step = LOOKUP16(g_nearStep, distance); } ``` 修正後: ![reference link](https://i.imgur.com/gCTQsXt.png) ### fixed-point 牆面距離不正確 可觀察下面兩張圖,可發現 fixed-point 在不同牆面距離會有所差距,但 floating-point 不會有差距,因此可以推測是 fixed-point 有問題。 ![reference link](https://i.imgur.com/16nxIaF.png) ![reference link](https://i.imgur.com/3aBi75I.png) 觀察 `raycaster_float.cpp` 和 `raycaster_fixed.cpp` 中 `IsWall` 的差別。 可觀察到 `raycaster_fixed.cpp` ```c=61 inline bool RayCasterFixed::IsWall(uint8_t tileX, uint8_t tileY) { if (tileX > MAP_X - 1 || tileY > MAP_Y - 1) { return true; } return LOOKUP8(g_map, (tileX >> 3) + (tileY << (MAP_XS - 3))) & (1 << (8 - (tileX & 0x7))); } ``` `raycaster_float.cpp` ```c=16 if (tileX < 0 || tileY < 0 || tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) { return true; } return g_map[(tileX >> 3) + (tileY << (MAP_XS - 3))] & (1 << (8 - (tileX & 0x7))); ``` 可觀察到 `raycaster_fixed.cpp` 中的第 63 行的 if 判斷式並沒有判斷到等於的情況 因此修正後程式碼如下: `raycaster_fixed.cpp` ```c=61 inline bool RayCasterFixed::IsWall(uint8_t tileX, uint8_t tileY) { if (tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) { return true; } return LOOKUP8(g_map, (tileX >> 3) + (tileY << (MAP_XS - 3))) & (1 << (8 - (tileX & 0x7))); } ``` 修正後: ![reference link](https://i.imgur.com/69kS3cP.png) ### 發現 `g_map` 中地圖與實際不同 ![reference link](https://i.imgur.com/MgH9vrY.png) 觀察 `raycaster_float.cpp` 及 `raycaster_fixed.cpp` 可得知 `IsWall()` 是判斷障礙物與否,而障礙物資訊是紀錄在 `raycaster-data.h` 的 `g_map` 中 由 `IsWall()` 推斷 `g_map` 有關資訊如下: * A bit as a pixel. * 大小為正方形 (32x32) pixel, 四個 uint8_t element 為一個 X axis ,共分成 32 rows, that is, Y axis. 按照二維方向排列應該長這樣: ``` 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, ... 0b00000000, 0b00000000, 0b00000000, 0b00000000, ``` 橫向 32 bit,縱向 32 rows. **圖片大小推測** `raycaster_float.cpp` line:16 ```c=16 if (tileX < 0 || tileY < 0 || tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) { return true; } ``` `MAP_X` $=$ `MAP_Y` $= 32$ 由以上程式碼可推斷若 `tileX` 或 `tileY` $< 0$ 以及 `tileX` 或 `tileY` $>= 31$ ,則為超過邊界 因此可得知地圖大小為 $32*32$ `raycaster_float.cpp` line:19 ```c=19 return g_map[(tileX >> 3) + (tileY << (MAP_XS - 3))] & (1 << (8 - (tileX & 0x7))); ``` * 此段程式碼為判斷座標 (`tileX`, `tileY`) 是否為障礙物,由於 `g_map` 中,每個 element 皆為 `uint_8` ,因此每個 element 可以代表 8 個 pixel,$8=2^3$,因此 `tileX` 需除 8 ,也就是 >> 3 。 * `g_map` 是 1d array ,因此要計算 Y 方向應除掉 X 方向的長度, X 方向共包含 32 個 bit ($32=2^5$) ,但是一個 element 可以代表 8 個 bit ,因此,應除 $32/8=4$ 或 `>> 4`。 * `MAP_XS` 的意義為地圖大小的 2 指數,即 `2^5=32` ,其中 32 為 `MAP_X` ,`MAP_XS` 為指數部分。 * 第 18 行用來取 X axis 的 bit ,需注意的是,地圖中 LSB 為 X 正向, MSB 為 X 反向,與數值大小方向相反。取 7 的原因為,一個 element 可以代表 8 個 bit,這 8 個 bit 恰好是 X 座標的 3 個 LSBs 因此取 3 個 LSB 全為 1 的情況,即為 `0x7`。 :::warning 這邊的 `8-(tileX & 0x7)` 應該錯了,考慮 `tileX` 最大即最小情況: - 最小 `tileX` =0 時,`8-0=8`:1<<8,會超過 uint8_t 的範圍 - 最大 `tileX` =7 時,`8-7=1`:1<<1=2,發現永遠沒有 1 的情況 因此應該將 8 修正為 7。 ::: 修正後可發現障礙物位置與 `g_map` 相同