--- tags: Linux2020 --- # 2020q3 Homework5 (render) contributed by < `YLowy` > ## raycaster 程式碼 額外整理成此篇 [raycaster 程式碼解釋](https://hackmd.io/@YLowy/ry-B-5x_w) - [ ] 修正浮點數和定點數算繪程式展現的缺失,並提出改進 precision 及 accuracy 的方式 ### float-point 運算缺失 ![](https://i.imgur.com/cx8EPJf.png) :::info 分析問題點 : 只有在距離牆壁很近的情況會發生,中間的牆壁大小突然變成 0 ,所以可以朝這點觀察關於 `sso` 的程式碼。 另外變形牆壁的起點也有問題,其中`tso`也是需要觀察的地方 ::: 觀察計算 sso 大小的程式碼 Trace method ```cpp=31 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; } ``` 第`51`行中,會透過計算物件與角色距離計算`*screenY ` ,也就是 sso。基本上問題點就是在段,畢竟這就是唯一計算與物體距離的程式碼。 在`distance` 無限小但不是 0 的情況下,`uint8_t` 的`*screenY `會是一個很大的數值,但是該數值最大值只能達到 255 之後的計算均會用到 ${\frac{畫在視窗牆壁大小}{視窗大小}}$ 這個比例值,然而在原始程式碼中接近至一個距離後,該比例只會為 `uint8_t`最大值 = 255,造成後面計算 `*textureStep` `*textureY`的計算出錯 #### 程式碼改進 需要改進部分有兩點 : 1. `*screenY` 為外部要牆壁的線條長度,由於需要計算多少像素,所以回傳的型態為`uint8_t`,但是當把實際 `INV_FACTOR / distance` 的時候,他已經失去原先浮點數計算該可以含括範圍,尤其在非常接近物體`distance`非常小的情況下`*screenY` 只能顯示到 255,為了確保精度應該將`INV_FACTOR / distance` 指派給一個浮點值 2. 當牆壁物件全部進入視窗時候 `sso` 應該為佔全部的視窗大小`SCREEN_HEIGHT >> 1` 修改過後 : ```c=51 if (distance > 0) { *screenY = INV_FACTOR / distance; auto txs = (INV_FACTOR / distance * 2.0f); // 修改 if (txs != 0) { *textureStep = (256 / txs) * 256; if (txs > SCREEN_HEIGHT) { *screenY = SCREEN_HEIGHT >> 1; // 新增 auto wallHeight = (txs - SCREEN_HEIGHT) / 2; *textureY = wallHeight * (256 / txs) * 256; } } } else { *screenY = 0; } ``` 以上程式碼會遇到個問題 `INV_FACTOR / distance` 計算會被重複用到太多次 以及 `256 / txs`浮點數除法運算 這兩點都會造成效能低落降低 (不考慮 gcc 編譯最佳化 `-O2`) 以下為針對上述修改程式精簡版本 : ```c=51 if (distance > 0) { float Screenrate = INV_FACTOR / distance; *screenY = static_cast<int8_t>(Screenrate); auto txs = (Screenrate * 2.0f); if (txs != 0) { *textureStep = (256 / txs) * 256; if (txs > SCREEN_HEIGHT) { *screenY = HORIZON_HEIGHT; auto wallHeight = Screenrate - HORIZON_HEIGHT; *textureY = wallHeight * (256 / txs) * 256; } } } else { *screenY = 0; } ``` 修改過後 floating point 近處的變形已經完成 ![](https://i.imgur.com/1gP1rvY.png) ### fixed-point 邊界問題 ![](https://i.imgur.com/vUWsQpf.png) 正確情況應該要像右圖,牆壁應該要接在一起 ![](https://i.imgur.com/r3zztj9.png) 問題分析 : 程式碼中 IsWall() 判斷邊界部分出錯 #### 程式碼改進: `(tileX > MAP_X - 1 || tileY > MAP_Y - 1)` 改成 `(tileX >= MAP_X - 1 || tileY >= MAP_Y - 1)` 即可 ```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))); } ``` ![](https://i.imgur.com/NqFvmeA.png) 改動完上述程式碼可以發現邊界牆壁已正確 - [ ] 輸出算繪過程的 frame rate,日後當我們進一步提升算繪程式的效率時,這會是效能評比的方式之一 ## 新增 frame rate 畫面 修改部分可以看 : [GitHub](https://github.com/YLowy/raycaster) 考慮 `DrawBuffer()`對相同大小的 buffer 做繪製,該執行時間都相同,所以只需要分別測兩者 `Trace` method ,透過 bench 測得 float 以及 fixed 的 Trace 進行測量計算時間即可 這邊我做了點好玩的事,做了個frame rate 的比較圖,這樣不管角色移動到哪裡都可以透過另外的視窗觀察兩者差異 SDL 繪圖 顯示 renderer 視窗分別所需要花費時間,加上 Homework 6 提到 [Ring Buffer](https://hackmd.io/@YLowy/SkDR4AIDD#%E6%B8%AC%E9%A9%97%E4%BA%8C-ring-buffer-%E5%AF%A6%E4%BD%9C) 的概念實作下圖這樣的顯示介面 (上面為 float 下方為 fixed) ![](https://i.imgur.com/bl0u0XP.png) 可以在模擬器中分別看出當前場景以及之前的場景在 float 以及 fixed raycaster 所需時間 ![](https://i.imgur.com/yXWyAoV.png) ![](https://i.imgur.com/zpXDAXs.png) 透過上述程式可以清楚觀察到,在 **float 計算遠處場景時**所消耗時間會突然上升到 **顯示近處所需時間 * 1.25**,其他倒是挺穩定的 也許可以嘗試近處使用 float 計算,遠處使用 fixed 計算,不過如何判斷遠近是個問題。 :::info 這裡只做到陽春的執行時間比較,畢竟 **花費時間比較** 可能才是想知道的 之後會補這些 : 1. 座標軸以及單位 2. 框架文字 3. 改成折線圖,這樣比較有感覺 4. 修改取樣時間 ::: - [ ] 利用 tools/precalculator.cpp 產生運算表格,修改相關程式碼,使得程式碼在編譯時期才去產生運算表格,後者以標頭檔案 (generated header) 的形式存在並編譯進入主程式。換言之,檔案 raycaster_tables.h 應自 repository 移除,改用編譯時期產生 ## 初始化時期產生 Table 在 fixed 的計算中常常利用到查表以獲得該距離,這個表格是透過 precalculator.cpp 產生的 :::info 你的程式碼應該透過程式碼寫出來 -不知道誰講過這句話 ::: 所以若希望移除 raycaster_tables.h ,則需要透過在程式碼初始化時期先創立空間以先寫入陣列 這邊我創立一個提供查表物件 `Precal_table` ```cpp= class Precal_table { public: uint16_t g_tan[256]; uint16_t g_cotan[256]; uint8_t g_sin[256]; uint8_t g_cos[256]; uint8_t g_nearHeight[256]; uint8_t g_farHeight[256]; uint16_t g_nearStep[256]; uint16_t g_farStep[256]; uint16_t g_overflowOffset[256]; uint16_t g_overflowStep[256]; uint16_t g_deltaAngle[SCREEN_WIDTH]; void Precal_table_Init(); Precal_table(); ~Precal_table(); }; ``` 在 `main.cpp` 中會在執行遊戲前進行 ` pctable.Precal_table_Init();` 完成初始化,將查表資料寫入該物件的陣列中 之後 `raycaster_fixed.cpp` 中透過 `extern` 引用外部文件的公共變數 [GitHub](https://github.com/YLowy/raycaster) 目前已經把 raycaster_tables.h 以及 tools檔案移除,透過程式在initialize 執行時期建立所有表格資料 - [ ] 解說現有 fixed-point 實作機制,並探討前述表格產生的機制,需要提及其中的考量點 參考 [程式碼解釋](https://hackmd.io/@YLowy/ry-B-5x_w) - [ ] 參照 C 語言:物件導向程式設計篇,透過建立共用介面 (interface) 的手法,將 raycaster 以 C99/C11 (或 gnu99/gnu11) 重寫,允許在執行時期載入 fixed-point 和 floating-point 為基礎的 renderer - [ ] 閱讀 Doom rendering engine 和 Casting Wolf3D-style Rays with an FPGA and Arduino,解釋 raycaster 運作原理,並探討後續的改進方案,例如更換彩色的貼圖素材 (texture)、引入遊戲元素 (sprite) 等等。