--- tags: Linux2020 --- # 2020q3 Homework5 (render) 程式碼解釋 contributed by < `YLowy` > [Github](https://github.com/YLowy/raycaster) [2020q3 Homework5 (render)](https://hackmd.io/@YLowy/S196k01uv) ## SDL (Simple DirectMedia Layer) 1. 一款跨平台的多媒體開發程式庫 2. C 語言撰寫並封裝以支援各種作業系統 3. SDL 2.0 API 不與下向相容 以下均參考 [SDL 官網](https://www.libsdl.org/) ## main.cpp :::spoiler int main(int argc, char *args[]) ```c=66 int main(int argc, char *args[]) { if (SDL_Init(SDL_INIT_VIDEO) < 0) { printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError()); } else { SDL_Window *sdlWindow = SDL_CreateWindow("RayCaster [fixed-point vs. floating-point]", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_SCALE * (SCREEN_WIDTH * 2 + 1), SCREEN_SCALE * SCREEN_HEIGHT, SDL_WINDOW_SHOWN); if (sdlWindow == NULL) { printf("Window could not be created! SDL_Error: %s\n", SDL_GetError()); } else { Game game; RayCasterFloat floatCaster; Renderer floatRenderer(&floatCaster); uint32_t floatBuffer[SCREEN_WIDTH * SCREEN_HEIGHT]; RayCasterFixed fixedCaster; Renderer fixedRenderer(&fixedCaster); uint32_t fixedBuffer[SCREEN_WIDTH * SCREEN_HEIGHT]; int moveDirection = 0; int rotateDirection = 0; bool isExiting = false; const static auto tickFrequency = SDL_GetPerformanceFrequency(); auto tickCounter = SDL_GetPerformanceCounter(); SDL_Event event; SDL_Renderer *sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_ACCELERATED); SDL_Texture *fixedTexture = SDL_CreateTexture( sdlRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT); SDL_Texture *floatTexture = SDL_CreateTexture( sdlRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT); while (!isExiting) { floatRenderer.TraceFrame(&game, floatBuffer); fixedRenderer.TraceFrame(&game, fixedBuffer); DrawBuffer(sdlRenderer, fixedTexture, fixedBuffer, 0); DrawBuffer(sdlRenderer, floatTexture, floatBuffer, SCREEN_WIDTH + 1); SDL_RenderPresent(sdlRenderer); if (SDL_PollEvent(&event)) { isExiting = ProcessEvent(event, &moveDirection, &rotateDirection); } const auto nextCounter = SDL_GetPerformanceCounter(); const auto seconds = (nextCounter - tickCounter) / static_cast<float>(tickFrequency); tickCounter = nextCounter; game.Move(moveDirection, rotateDirection, seconds); } SDL_DestroyTexture(floatTexture); SDL_DestroyTexture(fixedTexture); SDL_DestroyRenderer(sdlRenderer); SDL_DestroyWindow(sdlWindow); } } SDL_Quit(); return 0; } ``` ::: ### 遊戲執行前的初始化行為 #### int SDL_Init(Uint32 flags) >Use this function to initialize the SDL library. This must be called before using most other SDL functions. | Flag | description | | --------------- | --------------------------------------------------------------- | | SDL_INIT_TIMER | timer subsystem | | SDL_INIT_AUDIO | audio subsystem | | SDL_INIT_VIDEO | video subsystem; automatically initializes the events subsystem | | SDL_INIT_EVENTS | events subsystem | | ... | ... | 第 `68` 行會對 SDL_INIT_VIDEO FLAG 對 SDL所對應結構初始化,以下才能對影像進行處理。 #### SDL_Window* SDL_CreateWindow (const char* title, int x, int y, int w, int h, Uint32 flags) >Use this function to create a window with the specified position, dimensions, and flags. | parameter | description | | -------- | -------- | | title | 視窗標題 | | x, y | x, y position | | w, h | width, height | | flags | SDL_WindowFlags | 可以產生運作畫面的 window。 第 `75` 行所使用到的 FLAG `SDL_WINDOW_SHOWN` >SDL_WINDOW_SHOWN is ignored by SDL_CreateWindow(). >The SDL_Window is implicitly shown if SDL_WINDOW_HIDDEN is not set. 簡單而言就是讓視窗是可以顯示的。 第 `81` 行之後會對應 fixed-point 和 floating-point 進行畫面分配。 參照程式碼中第 `81` 行 ~ 第 `102` 行 第 `81` 行 game 指向 Game 結構,詳細部分放在下方 game.cpp,這裡可以先理解為該結構紀錄腳色位置以及角度。 第 `82` 行 ~ 第 `87` 行 分別做出 float 以及 fixed 的 Caster , 以及兩個大小為 `SCREEN_WIDTH * SCREEN_HEIGHT buffer` 以 `DrawBuffer()` 描繪出個所顯示畫面。 第 `88` 行 ~ 第 `90` 行 `moveDirection` `rotateDirection`初始化遊戲角色的移動方向以及轉動方向 `isExiting` 判斷遊戲執行中與否 第 `91` 行 ~ 第 `102` 行 #### SDL_GetPerformanceFrequency() >Use this function to get the count per second of the high resolution counter. 第 `91` 行 `tickFrequency` 取得遊戲禎數 #### SDL_GetPerformanceCounter() >Use this function to get the current value of the high resolution counter. 第 `92` 行 `tickCounter` 取得當前 high resolution counter 的值 #### SDL_Event() >A union that contains structures for the different event types. 第 `93` 行 `event` 結構中有許多種類 [參考](https://wiki.libsdl.org/SDL_Event) 第 `95` 行 ~ 第 `102` 行 #### SDL_Renderer >A structure that contains a rendering state. #### SDL_CreateRenderer >Use this function to create a 2D rendering context for a window. 讓前述所創的 window 建立 2D rendering 行為 #### SDL_Texture >A structure that contains an efficient, driver-specific representation of pixel data. #### SDL_CreateTexture >Use this function to create a texture for a rendering context. 對 fixed 以及 float 均建立在前者 window 所創立之 render,並且確定其 flamework | parameter | description | |:--------- |:--------------------------------------------------- | | renderer | the rendering context | | format | one of the enumerated values in SDL_PixelFormatEnum | | access | one of the enumerated values in SDL_TextureAccess | | w | the width of the texture in pixels | | h | the height of the texture in pixels | FPS 會經常會更換畫面故 access : `SDL_TEXTUREACCESS_STREAMING` ### 遊戲執行中的動作行為 第 `104` 行 ~ 第 `123` 行 : 第 `104` 行 ~ 第 `110` 行 使 window 上顯示 2 個 flamework #### SDL_RenderPresent(sdlRenderer); >Use this function to update the screen with any rendering performed since the previous call. 第 `112` 行 對畫面更新 #### SDL_PollEvent >Use this function to poll for currently pending events. 第 `114` 行 以 `ProcessEvent` 對 `event.key.keysym.sym` 判斷對當前本身人物狀態更新例如前進後退或者改變角度 第 `118` 行 ~ 第 `121` 行 : :::info 稍微解釋一下這邊在幹嘛 : 第 `91` 行 `const static auto tickFrequency = SDL_GetPerformanceFrequency();` tickFrequency 為一個靜態變數紀錄該 SDL 平台每秒的記數值 1. 第 `92` 行 `auto tickCounter = SDL_GetPerformanceCounter();` tickCounter 為初始化時 SDL counter 當前的值 2. 第 `118` 行 第 `119` 行 遊戲主階段時可以得知每次 while 迴圈執行時,透過 `nextCounter - tickCounter` 得到該迴圈對於 SDL 平台的 counter 計算了少次。 3. 對於計算 `(nextCounter - tickCounter) / static_cast<float>(tickFrequency)` 一輪迴圈的 counter / 一秒鐘的 counter = 一個迴圈執行的時間(秒) 這樣好處為不會因為電腦效能好壞影響整體遊戲的前進轉動速度。 ::: #### game.Move() 對角色進行位移以及轉動,會需要上面所計算的 `seconds` 以及在 ProcessEvent 對輸入信號進行偵測以及運算的 `moveDirection` 移動方向 `rotateDirection` 旋轉方向 ## game.[cpp/h] ### game.h :::spoiler class Game ```cpp= class Game { public: void Move(int m, int r, float seconds); float playerX, playerY, playerA; Game(); ~Game(); }; ``` ::: Game 類別,其中包含可以供外部檔案存取的 1. float 變數 `playerX`, `playerY`, `playerA`, 2. Method `void Move(int m, int r, float seconds)` 3. 該類別的 construction 以及 deconstruction > 誰創的誰就該回收 ### game.cpp :::spoiler game.cpp ```cpp= void Game::Move(int m, int r, float seconds) { playerA += 0.05f * r * seconds * 25.0f; playerX += 0.5f * m * sin(playerA) * seconds * 5.0f; playerY += 0.5f * m * cos(playerA) * seconds * 5.0f; while (playerA < 0) { playerA += 2.0f * M_PI; } while (playerA >= 2.0f * M_PI) { playerA -= 2.0f * M_PI; } if (playerX < 1) { playerX = 1.01f; } else if (playerX > MAP_X - 2) { playerX = MAP_X - 2 - 0.01f; } if (playerY < 1) { playerY = 1.01f; } else if (playerY > MAP_Y - 2) { playerY = MAP_Y - 2 - 0.01f; } } Game::Game() { playerX = 23.03f; playerY = 6.8f; playerA = 5.25f; } Game::~Game() {} ``` ::: `playerA` 紀錄角色的面相角度,其範圍如下,對於每次更新而改變的面向角度都會控制在以下範圍。 ![](https://i.imgur.com/o2CHSIN.png) `playerX` `playerY` 為角色在地圖的確切位置,對於每次更新的位置可以透過下更新。 ![](https://i.imgur.com/ooG65vI.png) 距離會隨著時間以及速度而改變。 :::success **總結** **render** : 每一次刷新時候,game 紀錄角色資訊,透過角色位置以及面向角度決定兩個 buffer 陣列的值,兩個陣列會對應該內部資料進行圖像修改並且呈現。 ::: --- ## float Renderer **Renderer 物件** ![](https://i.imgur.com/35kDz4b.png) **RayCasterFloat 物件** 繼承 **RayCaster** ![](https://i.imgur.com/CRyjtQg.png) main.cpp 中的程式碼讓物件如下圖構成。 ```c=81 Game game; RayCasterFloat floatCaster; Renderer floatRenderer(&floatCaster); uint32_t floatBuffer[SCREEN_WIDTH * SCREEN_HEIGHT]; ``` ![](https://i.imgur.com/iMUsPfH.png) `game` 物件在下方解釋。 在 main() 迴圈中 `floatRenderer.TraceFrame(&game, floatBuffer);` 會對 buffer進行刷新。 首先看看 `TraceFrame()` 這個 method : ```c=7 _rc->Start(static_cast<uint16_t>(g->playerX * 256.0f), static_cast<uint16_t>(g->playerY * 256.0f), static_cast<int16_t>(g->playerA / (2.0f * M_PI) * 1024.0f)); ``` `_rc` 會在建立物件時透過 constructor 建立起來,這邊就是指向物件 **floatCaster**,並且使用到該物件的 method `Start()`。 放進 `Start()` 這個 method 的參數 : >角色 g 的 player X,Y,A 都是 float type >X,Y 都先乘 256 ( ${2^8}$ ) 再強制轉型成 unsigned 16 bits integer >所以傳入的 XY 值前 8 位數會是原本數的整數部分,後8位為小數部分 >playerA 原先值範圍為 0~2π,而傳入 method 的值會變成等比放大的 0~1024 再來看看該物件的 Method Start() ```c=165 void RayCasterFloat::Start(uint16_t playerX, uint16_t playerY, int16_t playerA) { _playerX = (playerX / 1024.0f) * 4.0f; _playerY = (playerY / 1024.0f) * 4.0f; _playerA = (playerA / 1024.0f) * 2.0f * M_PI; } ``` _playerX,Y,A 為 float type 但是又把之前的操作變回去了? :::info 不了解為什麼要這樣,乾脆傳送 float 值,再將 x y 乘4.0就好了吧? 解答 : 考慮到以最小改動方式更新兩邊不同程式碼,且希望在 fixed point 不要有接觸到浮點數運算的機會,所以在這裡做轉換兩次轉回自己。 ::: 再來繼續觀察 `Trace` 程式碼,第 `11` 行有個 `for (int x = 0; x < SCREEN_WIDTH; x++) {` 其目的就是要對 buffer 做更新寫入。每次迴圈會更新該 **行** ![](https://i.imgur.com/FtmAX0n.png) ```c=12 uint8_t sso; uint8_t tc; uint8_t tn; uint16_t tso; uint16_t tst; uint32_t *lb = fb + x; _rc->Trace(x, &sso, &tn, &tc, &tso, &tst); ``` 再來會透過 **floatCaster** 這個物件的 **Trace** Method ,對於每行 buffer,可以透過這個 method 取得該行的 所有 buffer的資料以上色。 也就是說這邊會根據 x 輸入不同取回每行的 `sso` `tn` `tc` `tso` `tst`,這裡可以先想一下要繪下圖該行需要那些條件。 ![](https://i.imgur.com/25ve2pQ.png) `void RayCasterFloat::Trace` 這個部分運作會在下方提到。 sso 為該行牆壁的大小,也就是上圖灰色部分。 這邊會使得 `sso` 做調整,考慮如果牆壁顯示會超出上下界的情形,而`ws`為牆壁以外的部分大小。 另外遊戲設計時會是上下對稱,`HORIZON_HEIGHT` 以及`sso`都是表示天空地板部分的分配 ```c=22 int16_t ws = HORIZON_HEIGHT - sso; if (ws < 0) { ws = 0; sso = HORIZON_HEIGHT; } ``` 再來會從該行最上面的 buffer 開始填值,`GetARGB`可得到有透明感的 ARGB,且會隨著 y 提升亮度減低 (比較遠的天空比較暗)。 `lb += SCREEN_WIDTH;` 這表示要準備該行下個 buffer 位置。 ```c=30 for (int y = 0; y < ws; y++) { *lb = GetARGB(96 + (HORIZON_HEIGHT - y)); lb += SCREEN_WIDTH; } ``` 上層天空完成後再來換中間的牆壁部分,其大小為` 2*sso` ![](https://i.imgur.com/M98Or0M.png) `tn` texture number 可以載入不同的牆面種類,這裡考慮亮暗面牆壁 `tc` 該行 x 對應到 texture 矩陣的值 `tso` 考慮到視窗沒辦法顯示超出視窗的牆壁時候的狀況 `tst` 程式碼中運用到的牆壁其 64 * 64 矩陣,在遠近情況不同時有不同的對應顯示方法 `uint16_t ts = tst;` 無號 16 bits 整數以描述 tst 牆壁很遠時 : 在遠處的牆壁,如果只能以 32 * 32 大小以描述 64 * 64 ${tst = \frac{64}{32}}$ 牆壁很近時 : 在近處的牆壁,如果只能以 256 * 256 大小以描述 64 * 64 ${tst = \frac{64}{235}}$ 使用到 raycaster_data.h 的 g_texture8 牆壁陣列 ```c=35 for (int y = 0; y < sso * 2; y++) { // paint texture pixel auto ty = static_cast<int>(to >> 10); auto tv = g_texture8[(ty << 6) + tx]; //這表示在64*64 牆壁矩陣中 該行(ty)*64 +tx = 當下的pixel to += ts; if (tn == 1 && tv > 0) { // dark wall tv >>= 1; } *lb = GetARGB(tv); lb += SCREEN_WIDTH; } ``` 最後地板部分與天空原理相同 ```c=50 for (int y = 0; y < ws; y++) { *lb = GetARGB(96 + (HORIZON_HEIGHT - (ws - y))); lb += SCREEN_WIDTH; } ``` 以上就完成該行的從上而下刷新每個 buffer,將每行更新後就可以透過 `DrawBuffer()` 呈顯。 Trace method ```c=140 float deltaAngle = atanf(((int16_t) screenX - SCREEN_WIDTH / 2.0f) / (SCREEN_WIDTH / 2.0f) * M_PI / 4); ``` ![](https://i.imgur.com/HlTa0UP.png) 可以透過帶入 screenX 最大最小值 (0 ~ SCREEN_WIDTH ) 得到該視角為 90° `deltaAngle` 範圍為 -1 ~ 1 這邊會用到我們在 object construct 階段存的值 _playerX, Y, A ```c=142 float lineDistance = Distance(_playerX, _playerY, _playerA + deltaAngle, &hitOffset, &hitDirection); float distance = lineDistance * cos(deltaAngle); ``` ![](https://i.imgur.com/EGYKkbC.png) 有了這個 `distance` 我們先看下面的部分 ```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; } ``` 考慮地圖與人物關係,物體越遠,牆壁越小,也就是回傳的 `sso` 值會越小 而縮放大小比例 (`INV_FACTOR` ) 與平行人物距離成正比 `txs = (*screenY * 2.0f);` `*textureY = wallHeight * (256 / txs) * 256;` 這邊可以想為角色與眼前視窗距離 `#define INV_FACTOR (float) (SCREEN_WIDTH * 95.0f / 320.0f)` 另外回傳 `textureY` 為牆壁的 Y 軸起始位置。 `*textureStep = (256 / txs) * 256;` 先前提到的遠近像素問題,也和平行人物距離有關 如果該區沒有牆壁,則傳回 0 ![](https://i.imgur.com/dQloNWX.png) 視窗所呈現牆壁大小 (sso) 與垂直距離之關係 : 考慮下方張圖 : ![](https://i.imgur.com/0VV4BGX.png) 下圖可以更清楚顯示畫在視窗上牆壁大小與距離的比例關係 ![](https://i.imgur.com/4gEEnO7.png) ![](https://i.imgur.com/8k3srn9.png) 再來觀察 Trace method 所用到的 Distance method `tileStepX` `tileStepY` 用以判斷角色轉向 `rayA` 象限 ![](https://i.imgur.com/APwMiCp.png) `modff()` 用來分割浮點數的整數小數部分 以下計算角色到物體之距離 ![](https://i.imgur.com/FyXx25T.png) ![](https://i.imgur.com/FRafpPu.png) ![](https://i.imgur.com/zcM2Mgx.png) 在所有地圖牆壁都為 1 * 1 單位為大小,為了找到牆壁物件距離角色本身的距離,可以透過上面方法找到,並且反射光線的牆壁只會下圖灰色圈圈的地方出現,所以我們可以透過 `stepX` `stepY` 計算 `stepX` `stepY` 為可以算下個可以反射光線的位置 ![](https://i.imgur.com/WAL78tQ.png) 當然我們也要考慮再不同面向的牆壁亮度 ![](https://i.imgur.com/0fkvDgG.png) 首先先判斷該路線牆壁物體是否在自己所站的單位格中 第 `92` 行中,先判斷其 tileStepY 決定面向方向的象限 首先判斷 原始位置+1 也就是本身個子是否存在牆壁物件 並透過`interceptY += stepY`判斷下個格子是否存在牆壁物件 `IsWall()` 會在下方介紹,這邊先想成該結果回傳是否存在牆壁物件 `verticalHit ` 如果該條路線存在牆壁則該值會被設定為 ture `rayX` `rayY` 為該條路徑下對應牆壁的 x,y 座標 `hitDirection ` ture 、false 表示亮暗面 >要注意一下影響明亮度包括兩個 >1. 遠近 >2. 是否朝向面光面 ```c=90 do { somethingDone = false; while (((tileStepY == 1 && (interceptY <= tileY + 1)) || (tileStepY == -1 && (interceptY >= tileY)))) { somethingDone = true; tileX += tileStepX; if (IsWall(tileX, interceptY, rayA)) { verticalHit = true; rayX = tileX + (tileStepX == -1 ? 1 : 0); rayY = interceptY; *hitOffset = interceptY; *hitDirection = true; break; } interceptY += stepY; } ``` 第`98` 行 `rayX = tileX + (tileStepX == -1 ? 1 : 0);` 當路徑找到牆壁時候,其反射光線的牆壁位置會在 "前一格位置上" Y 軸 完成之後換計算 X 軸 的部分 ```c=106 while (!verticalHit && ((tileStepX == 1 && (interceptX <= tileX + 1)) || (tileStepX == -1 && (interceptX >= tileX)))) { somethingDone = true; tileY += tileStepY; if (IsWall(interceptX, tileY, rayA)) { horizontalHit = true; rayX = interceptX; *hitOffset = interceptX; *hitDirection = 0; rayY = tileY + (tileStepY == -1 ? 1 : 0); break; } interceptX += stepX; } } while ((!horizontalHit && !verticalHit) && somethingDone); ``` `rayX` `rayY` 其中一個會是能表示整數的浮點數 ![](https://i.imgur.com/iUzxWsk.png) 而為何會存在那麼多判斷式,其原因挺簡單的就是在面向不同方位 (角色視角為90°) 時候物件距離會有不同的計算方式,其實原理都一樣。 ![](https://i.imgur.com/QZHlEOh.png) 以上就可以得到我們所求的距離 再來回頭看看之前還沒提到的 `IsWall()` method ``` c=6 bool RayCasterFloat::IsWall(float rayX, float rayY, float rayA) { float mapX = 0; float mapY = 0; float offsetX = modff(rayX, &mapX); float offsetY = modff(rayY, &mapY); int tileX = static_cast<int>(mapX); int tileY = static_cast<int>(mapY); ``` 傳入位置座標,觀察該座標是否有物體 這裡只需要考慮整數值即可,畢竟牆壁物件在這裡為 1 * 1 單位矩陣 `tileX ` `tileY ` 為其整數值 ```c=14 if (tileX < 0 || tileY < 0 || tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) { return true; } ``` 這裡考慮邊界情況 ![](https://i.imgur.com/GgG89CB.png) ```c=17 return g_map[(tileX >> 3) + (tileY << (MAP_XS - 3))] & (1 << (8 - (tileX & 0x7))); } ``` 再來考慮地圖內牆壁物件,raycaster_data.h 中的`g_map` 為一個 128 * 無號 8 bits integer 的 array 其中共包含 1024 個 bits 表示整個地圖所存在牆壁位置 ( 32 * 32 ) 這裡透過 `tileX ` `tileY ` 對陣列內容作查詢 參考下圖對陣列考慮查詢 : ![](https://i.imgur.com/LDDziC1.png) 地圖陣列為一維排序,所以要判斷該點的位置應該要找尋第 ${titleY * 32 + tilteX}$ 個 bits 對應到矩陣每 8 個 bits 為一個陣列,所以首先要找到該 bit 所存放的 block,也就是第 ${\frac{titleY * 32 + tilteX}{8}}$ 個陣列位置 `g_map[(tileX >> 3) + (tileY << (MAP_XS - 3))]` `MAP_XS` 為 5 ,由於單邊為 ${2^5}$ 格單位 `(1 << (8 - (tileX & 0x7))`為對該 8 bits 整數透過 mask 找尋目標位元,對 `tileX` 取 8 的餘數的概念 透過上述就可以得知該座標是否存在牆壁物件了 :::info 總結一下 **float Renderer** : 做個 2D 的就那麼累了,3D API 應該超刺激的 1. 如何將畫面呈現? >對應 buffer 紀錄所有 pixel,再透過 DrawBuffer() 繪製 2. 如何更新 buffer? >縮小成對每條 buffer 陣列刷新,每次決定該條的繪製情況 3. 如何知道每一條 X buffer 要畫甚麼? >牆壁所佔的比例、牆壁起始位置、牆壁物件的起始位置以及牆壁物件一個色彩單位對視窗所需要展現的數量 4. 如何知道牆壁所佔的比例? >透過距離以操作比例,距離越遠牆壁長度越小 5. 如何知道牆壁離角色多遠? >`Distance()` 這個 Method 會從角色往該角度開始找第一個碰到的牆壁位置座標 6. 如何知道有沒有碰到牆壁 >透過`IsWall()`判斷引用地圖是否存在牆壁物件 ::: --- ## fixed Renderer 原則上方式與流程與上述相同,這邊只提到不同於 float之計算。 與 float Renderer 相同架構,透過 TraceFrame 刷新 buffer ![](https://i.imgur.com/bPqOTZM.png) 首先 `_rc->Start(...)` `RayCasterFixed::Start()` 這邊與 float 操作不同 紀錄角色位置 (x,y) 會變成 16 bits 整數,前 8 放置整數部分,後 8 為小數 `_playerA` 為 0~1023 的數字代表面相的方位 值得注意的是`_viewQuarter = playerA >> 8;` ` _viewAngle = playerA % 256;` 分別記錄朝向的方位以及該方位的位移角度 再來`_rc->Trace(x, &sso, &tn, &tc, &tso, &tst);` 與 float 相同,在已知角色位置以及面向的情況下,計算每條 x buffer,該條 buffer 需要的資訊有 `sso` 呈現在視窗的牆壁大小 `tn` 牆壁類型 `tc` 對照應該使用物件牆壁的第幾條資訊 (x軸) `tso` 該從牆壁物件第幾個開始呈現 `tst` 一格pixel所需次數 `void RayCasterFixed::Trace(...)` 實作當中透過查表方式計算其面向視窗的範圍 其目的原理可以參考 float renderer 的 deltaAngle ```c=224 uint16_t rayAngle = static_cast<uint16_t>(_playerA + LOOKUP16(g_deltaAngle, screenX)); ``` 綠色部分為 `rayAngle ` 所表示範圍 ![](https://i.imgur.com/7kC0vfB.png) 透過查表我們發現我們所要求的前後範圍為 ±108 在我們所表示中將一圈 360° 表示為 1024 我們可以推測整體視線角度應該為 ${108 * \frac{360}{1024} * 2 = 76°}$ ```c=227 // neutralize artefacts around edges switch (rayAngle % 256) { case 1: case 254: rayAngle--; break; case 2: case 255: rayAngle++; break; } rayAngle %= 1024; ``` 處理計算邊界情況 `rayAngle ` 表示成 0-1023 的範圍,以計算應對角度的最近牆壁距離 ```c=242 CalculateDistance(_playerX, _playerY, rayAngle, &deltaX, &deltaY, textureNo, textureX); ``` 再來觀察 `CalculateDistance()` 此 method ```c=88 void RayCasterFixed::CalculateDistance(...) ``` `_playerX` `_playerY` `rayAngle` 分別代表角色位置以及要計算的方向 `deltaX` `deltaY` 為透過計算回傳的"該點該方向到牆壁物件的 X Y 座標距離 "值 `textureNo` `textureX` 與 float renderer 相同 ![](https://i.imgur.com/eM2Vk8a.png) 透過 `CalculateDistance()` 可以得到距離 `deltaX` `deltaY` 而我們在 `Start()` 階段就有將欲觀察角度拆成 面向象限 `_viewQuarter` 以及 該象限中的角度 `_viewAngle` 原本直線距離可以用不同於畢氏定理方法,透過下圖的分割可以拆成兩個三角函數的加法,可以避開開根號的浮點數計算,而三角函數的值可以透過查表找到 ![](https://i.imgur.com/fYq2tId.png) 而這邊程式碼的部分是透過 switch 面相相位以決定正負數處理 以上就可以得到該角度距離的值`distance` 在來就是將距離轉成牆壁物件的資訊,包含牆壁起始位置,顯示在視窗的牆壁大小以及一個牆壁像素的顯現次數 `#define MIN_DIST (int) ((150 * ((float) SCREEN_WIDTH / (float) SCREEN_HEIGHT)))` `MIN_DIST` = 187.5 在整數顯示為 187 ```c=286 if (distance >= MIN_DIST) { *textureY = 0; LookupHeight((distance - MIN_DIST) >> 2, screenY, textureStep); } else { *screenY = SCREEN_HEIGHT >> 1; *textureY = LOOKUP16(g_overflowOffset, distance); *textureStep = LOOKUP16(g_overflowStep, distance); } ``` 上述程式碼中首先判斷 `distance >= MIN_DIST` 其距離是否太近,如果不會太近我們可以放心的把 `*textureY ` 設為 0 ,因為距離足夠牆壁會全部顯示,並透過 `LookupHeight((distance - MIN_DIST) >> 2, screenY, textureStep);` 決定該距離時視窗顯現牆壁的大小 而若牆壁距離角色太近時,首先牆壁大小會占滿整個畫面所以 `*screenY` 等於 ${\frac{1}{2}} *$`SCREEN_HEIGHT`,然後欲繪出的牆壁起始物件起始點以及一個牆壁像素的顯現次數透過另張表以查詢 :::info Q : 這個判斷式式中,多近的程度才算近? 在這裡的距離 187 需要考慮地圖為 32 * 32 格的地圖,而且一格距離為 256 ,所以這裡距離大概等於 0.73 個牆壁物件長而已 ::: :::info 此外這邊可以注意到一點,在計算 **欲繪製牆壁物件起始 y 位置** 中如果太近(`distance >= MIN_DIST`)我們會查詢 `g_overflowOffset` 這張表 當近到 `distance >= MIN_DIST = 187` 時候,觀察這張表會發現 g_overflowOffset[187] = 87,代表該距離中牆壁物件會從 87 開始繪製,同樣道理我們可以推得如果我們要站在剛好可以完全顯示牆壁的位置處,則大概就是在牆壁前 187 的距離 ::: `LookupHeight()` 即是對範圍去查表得到參數 ```c=70 void RayCasterFixed::LookupHeight(uint16_t distance, uint8_t *height, uint16_t *step) { 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); } } ``` #### `CalculateDistance()` 用定點數以計算 Distance 部分還挺複雜的 // TODO