hankluo6
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 2020q3 Homework5 (render) contributed by < `hankluo6` > ## 程式原理 ### `game` 用來紀錄程式內玩家的各式資訊(分別為當前 X 座標、Y 座標以及旋轉的弧度),以及 `Move` 函式讓玩家移動。其座標範圍為 $0 \sim 30$ 單位,旋轉弧度範圍為 $0 \sim 2\pi$,超過的部份會被限制住。 特別要注意的是旋轉的座標軸是以逆時針方向增加: ![](https://i.imgur.com/SBr8KON.png) ### `render` 此程式用來將需要繪製的資訊儲存到 buffer 當中,而主要 raycasting 的部份則是由其成員 `_rc` 內的 `Trace` 函式進行。 首先會先透過 `_rc->Start` 初始化玩家資料,接著每次 `for` 迴圈都會對 Screen (指的是兩個畫面中的其中一個) 上的**每一個 column** 進行 `_rc->Trace`,並將相關參數回傳,這些參數代表螢幕上的這條 column 所要繪製的資訊。 ```cpp for (int x = 0; x < SCREEN_WIDTH; x++) { 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); ``` * `sso` 為箱子範圍的一半 * `tc` 為箱子的 texture 在該 x 位置的值 * `tn` 用來標示箱子為亮面或暗面 * `tso` 為箱子的 texture 在該 y 位置的值 * `tst` 用來計算箱子 texture 上相同紋理要重複的次數 * `lb` 為要繪製 pixel 的位置 ![](https://i.imgur.com/6Otc5R1.png) 從 `g_texture8[4096]` 可以知道箱子的 texture 大小為 64 * 64 pixel,並用前 6 個 bit 選取 y 軸,後 6 個 bit 選取 x 軸,但在遊戲中箱子大小會隨著遠近而改變,且可能超過 64 pixel,此時就需要將螢幕上的多個 pixel 共用同個 texture 上的 pixel,達成放大的效果。 ![](https://i.imgur.com/jRKeu2l.png) 如果仔細看 256 pixel 的 texture,會發現其中 x 與 y 座標有 4 個 pixel 在 4x4 的方格中都相同。 假設現在需要繪製 128 pixel 的箱子高度,`screenY = 64`,`txs = 128`,則 `textureStep = (256 / 128) * 256 = 512`,接著 `TraceFrame` 中可看到 `(to >> 10)` 可知道 1024 以下的數值會被 round 掉,表示 `to += ts;` 此行需跑 2 次才會更新成新的 texture pixel,此即為 `tst` 也就是 `textureStep` 的作用。 而 `textureX` 重複次數與 `textureY` 計算不同,程式的目標是把 $0 \sim 1$ 的 `hitoffset` 對應到 $0 \sim 64$ 的 texture coordinate 上,可以先看到 `*textureX = (uint8_t)(256.0f * modff(hitOffset, &dum))` 將一個範圍為 $0 \sim 1$ 的數值乘上 $256$,接著在 `TraceFrame` 中 `const auto tx = static_cast<int>(tc >> 2);` 會再將此結果除上 $4$,所以最後會得到 `hitOffset * 64`,也就是將 $0 \sim 1$ 映射到 $0 \sim 64$ 並去除掉後面兩個 bit,這樣當有箱子較近時,兩個相鄰的 column 上 `hitoffset` 會相近,對應要 $0 \sim 64$ 也會是相同的值;箱子較遠時 `hitoffset` 則較小,自然對應後的值也會不同。 ```cpp const auto tx = static_cast<int>(tc >> 2); int16_t ws = HORIZON_HEIGHT - sso; if (ws < 0) { ws = 0; sso = HORIZON_HEIGHT; } uint16_t to = tso; uint16_t ts = tst; ``` 因為 `HORIZON_HEIGHT` 為一半的 `SCREEN_HEIGHT`,所以 `ws` 為繪製天空及地板的長度,如果 `ws` 為 0 則表示該 column 上的 pixel 全為箱子的一部分,可以注意到天空與地板在畫面上佔有的空間的是相同的。 ```cpp for (int y = 0; y < ws; y++) { *lb = GetARGB(96 + (HORIZON_HEIGHT - y)); lb += SCREEN_WIDTH; } ``` 繪製天空的迴圈,`lb` 每次迴圈位置增加 `SCREEN_WIDTH`,表示往下移動一個 pixel。 ```cpp 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]; to += ts; if (tn == 1 && tv > 0) { // dark wall tv >>= 1; } *lb = GetARGB(tv); lb += SCREEN_WIDTH; } ``` 繪製箱子的程式在上面有介紹過了,要注意的是當 `tn` 為 1 時要畫暗色的牆壁,做法為將 pixel 上的數值除 2,這是因為 `GetARGB` 上每個 8 bit 的值皆代表其顏色的亮度。 而繪製地板的步驟則與天空同理。 ### `raycaster_float` 因為在 `renderer` 中呼叫 `Start` 函式時已經將數值轉為 fixed point 形式,故這邊要在轉換回 floating point。 `Distance` 函式因其他同學有很好的解釋,故不再贅述,此函式會回傳離目標最近的箱子之距離,這邊目標將會從 `column 0` 計算到 `column SCREEN_WIDTH`,對每一個 column 我們都發射一條看不見的 ray (射線),並透過此射線判斷撞擊時的位置、距離等資訊。 `Trace` 函式將會計算所需的參數: ```cpp float hitOffset; int hitDirection; float deltaAngle = atanf(((int16_t) screenX - SCREEN_WIDTH / 2.0f) / (SCREEN_WIDTH / 2.0f) * M_PI / 4); float lineDistance = Distance(_playerX, _playerY, _playerA + deltaAngle, &hitOffset, &hitDirection); float distance = lineDistance * cos(deltaAngle); ``` * `hitOffset` 會紀錄 ray 與箱子間 intersect 的位置 * `hitDirection` 紀錄為亮面或暗面(根據 ray 與箱子 intersect 為箱子的垂直面或水平面決定,與真實光線無關) * `deltaAngle` 為 ray 與玩家視角為中心之向量角度差 * `lineDistance` 為 ray 投射最近的箱子之間的距離 * `distance` 則為 `lineDistance` 對玩家視角中心之向量的投影長度 先來理解螢幕是如何繪出畫面的,玩家會有一個視野範圍稱為 [field of view (fov)](https://en.wikipedia.org/wiki/Field_of_view),概念就是玩家從眼睛開始,能看到前方範圍的最大角度。我們需要將看到的視野投影到某個平面上,而這個平面就是我們在螢幕上看到的畫面,可以參考下圖: ![](https://i.imgur.com/hfkjUdz.jpg) 而上述玩家視野為中心之向量指的是一從玩家 Camera 到 Near plane 中心之向量。 `deltaAngle` 為目前要處理的 ray 與玩家視角間的角度,我們可以先從 `raycaster.h` 中知道 fov 為 $\frac{\pi}{2}$ 弧度,所以其一半範圍為 $\frac{\pi}{4}$ 弧度,根據 $\arctan{(\frac{\pi}{4})}=1$ 可以知道 `SCREEN_WIDTH / 2.0f` 與玩家到平面的距離相等。 而 `screenX - SCREEN_WIDTH / 2.0f` 為目前 ray 在平面上的交點與平面中心的差,在搭配上已知的視野角度,可以計算出要增加的角度應為: ```cpp atanf((int16_t) screenX - SCREEN_WIDTH / 2.0f) / (SCREEN_WIDTH / 2.0f); ``` $$\Delta\theta = \arctan(\frac{x - W/2}{D})=\arctan(\frac{x-W/2}{W/2})$$ 我認為這邊 `M_PI / 4` 是多餘的,但因為此角度對 `Distance` 的影響不大(只會影響到有象限交換時的判斷),且 `M_PI / 4` 與 `1` 差距沒有很大,目前看不出有明顯的差異。 另外我們需要對與箱子的距離 `lineDistance` 計算其與平面垂直的分量長度: `float distance = lineDistance * cos(deltaAngle);`,這是為了要解決 fisheye effect 的問題。 >根據 [Lode's Computer Graphics Tutorial](https://lodev.org/cgtutor/raycasting.html) > >The fisheye effect is an effect you see if you use the real distance, where all the walls become rounded, and can make you sick if you rotate. ```cpp *textureX = (uint8_t)(256.0f * modff(hitOffset, &dum)); *textureNo = hitDirection; *textureY = 0; *textureStep = 0; ``` 此段在上面 `textureX` 的部分有說明。 ```cpp 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; } ``` 這邊主要計算要繪製的 y 軸範圍,可以從下圖看到我們需要求出螢幕上要顯示的高度 $h$ ![](https://i.imgur.com/0GkHxd0.png) 而箱子在遊戲座標中高度為 1,其一半為 0.5,透過相似三角形公式: $$ \frac{Width/2}{distance}=\frac{h}{0.5}\\ h=\frac{Width/2}{0.5} \times \frac{1}{D} $$ 因為 $h$ 的前項為常數,可以透過 marco 預先計算,此即為 `INV_FACTOR` 的值。 :::info 上述計算出來的 `INV_FACTOR` 應該為 80,但程式內設定為 95,不知道哪邊推論錯誤。 ::: 接著 `if` 部份計算依照 $h$ 的大小決定要繪製細節的程度,箱子越靠近玩家則需要繪製的紋理節點就越多,反之則越少。 ### `raycaster_fixed` `Trace` 所需參數與 `raycaster_point` 一樣,但其內部使用定點數來表示小數,用 16 bit 的 整數儲存,前 8 bit 為小數的整數部分,後 8 bit 為小數部分,而角度的部分將原本 $0 \sim 2\pi$ 映射到 $0 \sim 1024$,另外使用 `viewQuarter` 紀錄當前旋轉角度的象限、`viewAngle` 則記錄相對於該象限的旋轉角度: ```cpp uint16_t rayAngle = static_cast<uint16_t>(_playerA + LOOKUP16(g_deltaAngle, screenX)); ``` 此行與 `float` 版本的計算一樣,指示該對應的數值透過查表來取得。 ```cpp switch (rayAngle % 256) { case 1: case 254: rayAngle--; break; case 2: case 255: rayAngle++; break; } rayAngle %= 1024; ``` :::warning 研究中... ::: `CalculateDistance` 與 `Distance` 的概念一樣,會回傳 ray 碰觸到的牆壁之間的 x 距離與 y 距離,將結果分別存到 `delatX` 與 `deltaY`,要注意的是內部有用到小數的部分皆為 fixed point,其中的加 256 就是 float point 中加 1 的步驟。 函式內還特別將角度為 0, 90, 180, 270 的部分拆開來算,這是因為角度為 0 不需要計算到三角函數,只需簡單的迴圈就能計算,分開運算較能提升效能。 接著計算玩家與箱子間的歐幾里德距離,與 float 版本不同的是,去除掉根號操作(效能較差),而用三角函數計算: $$ distance = \Delta Y \times \cos \theta + \Delta X \times \sin \theta $$ 而底下的兩個 `if` 判斷式就是就是在算 `distance`,根據不同象限來計算,裡面 `INVERT` 在做 2's complement,用來將角度取負值。 ```cpp 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` 經過計算為 `187.5`,而 fixed point 中數值 1 代表實際數值 256,可以知道 `else` 為與箱子距離過近時的情形。當距離過近時,對應到 float 版本的 `if (txs > SCREEN_HEIGHT)`,該 column 需全部繪製箱子,所需繪製的一半高度就為 `SCREEN_HEIGHT >> 1`,而 `textureY` 與 `textureStep` 可以從 `precalculater` 中發現: ```cpp for (int i = 1; i < 256; i++) { auto txs = ((INV_FACTOR_INT / (float) (i / 2.0f))); auto ino = (txs - SCREEN_HEIGHT) / 2; g_overflowStep[i] = (256 / txs) * 256; g_overflowOffset[i] = ino * (256 / txs) * 256; } ``` 計算方式與 float 版本一樣,其中 `i` 為 `distance`,因為原本判斷式已經限制 `distance < 256`,所以這邊計算 $1 \sim 255$ 就能涵蓋到所有此情形的數值。要特別注意的是 `INV_FACTOR_INT` 與 `i` 皆為定點數表示,而兩個定點數相除後的結果 `txs` 為**浮點數**,因為定點數的 MSB 8 bit 相除就抵銷掉原先位移的效果,所以可以直接利用 `txt` 計算 `textureY` 與 `textureStep`。 `LookupHeight` 會先將 `(distance - MIN_DIST) >> 2` 傳入到 `distance`: ```cpp 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); } } ``` 根據 `distance` 的值分成較近距離的情形與遠距離的情形,先看 `nearHeight` 與 `farHeight` 的計算方式: ```cpp for (int i = 0; i < 256; i++) { g_nearHeight[i] = static_cast<uint8_t>( (INV_FACTOR_INT / (((i << 2) + MIN_DIST) >> 2)) >> 2); g_farHeight[i] = static_cast<uint8_t>( (INV_FACTOR_INT / (((i << 5) + MIN_DIST) >> 5)) >> 5); } ``` 如果仔細看 `LookupHeight` 會發現計算 `farHeight` 前會將 `distance` 右移 5 bit(呼叫參數時 2 bit + `ds` 3 bit) 且減去 `MIN_DIST`,對應到上面程式中的 `(i << 5) + MIN_DIST`,因此處計算需要真實的 `distance`,而非 $0 \sim 256$;`nearHeight` 也一樣要把位移的 2 bit 給補回去。而 `MIN_DIST` 後方的兩次 shift 推測是做 round 的動作。 `nearStep` 與 `farStep` 就很簡單了,計算方式與 float 版本一樣,將 `farHeight` 或 `nearHeight` 的值 * 2,並計算在 texture 中對應的位置: ```cpp for (int i = 0; i < 256; i++) { auto txn = ((INV_FACTOR_INT / (((i * 4.0f) + MIN_DIST) / 4.0f)) / 4.0f) * 2.0f; if (txn != 0) { g_nearStep[i] = (256 / txn) * 256; } auto txf = ((INV_FACTOR_INT / (((i * 32.0f) + MIN_DIST) / 32.0f)) / 32.0f) * 2.0f; if (txf != 0) { g_farStep[i] = (256 / txf) * 256; } } ``` :::info 為甚麼要這麼麻煩分成三段距離來算同樣的數值呢?因為對於越遠的牆壁,我們所需要的精度越低,對應到 fixed point 的 LSB 8 bit,越遠的牆壁可以將之位移去除掉部分精度。 ::: 再回頭看 `LookupHeight` 應該就能理解了,而對於過遠的牆壁沒紀錄在表格內的數值,需要特別將他壓縮到 `ds = 255` 內以對應表格中最遠距離。 :::warning `LookupHeight` 當中 ```cpp 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` 才正確 ::: ## 修正浮點數和定點數算繪程式展現的缺失 ### 牆壁分割的問題 ![](https://i.imgur.com/z7teJoa.png) 此為距離牆壁時才會發生的問題,且推測為中間 `screenY` 比真實值還小。從 `Trace` 中可發現 `*screenY = INV_FACTOR / distance;` 會發生 overflow 的問題,因 `distance 可能非常小。另外對於 `txs > SCREEN_HEIGHT` 時沒有對 `screenY` 做相對應的處理,比照 `fixed point` 版本增加 `*screenY = SCREEN_HEIGHT >> 1;` 此行。 ```cpp if (distance > 0) { uint16_t t = INV_FACTOR / distance; *screenY = t; auto txs = (t * 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; } ``` ### 牆壁消失的問題 比對 fixed point 與 float point 版本就能知道: ```cpp if (rayA <= M_PI_2) { startDeltaX = (1 - offsetY) * tan(rayA); startDeltaY = (1 - offsetX) / tan(rayA); } else if (rayA <= M_PI) { if (offsetY == 0) { startDeltaX = (1) * fabs(tan(rayA)); } else { startDeltaX = (offsetY) *fabs(tan(rayA)); } startDeltaY = -(1 - offsetX) / fabs(tan(rayA)); } else if (rayA < 3 * M_PI_2) { if (offsetY == 0) { startDeltaX = -(1) * fabs(tan(rayA)); } else { startDeltaX = -(offsetY) *fabs(tan(rayA)); } if (offsetX == 0) { startDeltaY = -(1) / fabs(tan(rayA)); } else { startDeltaY = -(offsetX) / fabs(tan(rayA)); } } else { startDeltaX = -(1 - offsetY) * fabs(tan(rayA)); if (offsetX == 0) { startDeltaY = (1) / fabs(tan(rayA)); } else { startDeltaY = (offsetX) / fabs(tan(rayA)); } } ``` `offsetX` 的對於 0 的判斷可以去除,當站在表格邊界上時,應要以當前表格邊界開始投射 ray,而非下個表格。 ### 牆面比例不同的問題 可以發現外牆兩邊的比例不同,透過下面這張圖推測應為 fixed point 出錯,對照兩個版本的 `isWall` 可以找出判斷式的差異,修改後的程式如下: ```cpp 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))); } ``` ### 牆壁錯位的問題 此問題在上面分析程式碼時就發現了,此處只是利用 `distance >= 256` 時的情況重現此 bug。當 `distance >= 256` 時,`LOOKUP(g_farXXXX, dx)` 將會取值到不屬於該 array 的內容: ```cpp 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); } ``` :::warning 特別注意 C 及 C++ 不會有 Out Of Bounds Error,而是 Undefined Behavior! ::: ## 在編譯時期產生運算表格 使用 C++ 的 template 實現 [Template metaprogramming](https://en.wikipedia.org/wiki/Template_metaprogramming),使用 template 的技巧來讓 compiler 在編譯時期計算結果。 我們需要使用 template 來實現 loop 並產生資料,可以先從 [Wikipedia](https://en.wikipedia.org/wiki/Template_metaprogramming#Static_Table_Generation) 的例子來思考: ```cpp= #include <iostream> #include <array> constexpr int TABLE_SIZE = 10; /** * Variadic template for a recursive helper struct. */ template<int INDEX = 0, int ...D> struct Helper : Helper<INDEX + 1, D..., INDEX * INDEX> { }; /** * Specialization of the template to end the recursion when the table size reaches TABLE_SIZE. */ template<int ...D> struct Helper<TABLE_SIZE, D...> { static constexpr std::array<int, TABLE_SIZE> table = { D... }; }; constexpr std::array<int, TABLE_SIZE> table = Helper<>::table; enum { FOUR = table[2] // compile time use }; int main() { for(int i=0; i < TABLE_SIZE; i++) { std::cout << table[i] << std::endl; // run time use } std::cout << "FOUR: " << FOUR << std::endl; } ``` 要看懂上面程式之前,需先理解 [Variadic template](https://en.wikipedia.org/wiki/Variadic_template) 以及 [Partial template specialization](https://en.wikipedia.org/wiki/Partial_template_specialization),網路上相關資料很多,這邊就不再贅述。 當 compiler 看到此行時 ```cpp=20 constexpr std::array<int, TABLE_SIZE> table = Helper<>::table; ``` 會先尋找對應到 `Helper<>` 的 Object,發現到 ```cpp=9 template<int INDEX = 0, int ...D> struct Helper : Helper<INDEX + 1, D..., INDEX * INDEX> { }; ``` 此 object 為符合呼叫之結構,而需要回傳此結構前需先建構此 `struct`,故需要先繼承 `Helper<INDEX + 1, D..., INDEX * INDEX>`,而 variadic template 會自動將後方的 `INDEX * INDEX` 放入 `D...` 中呼叫(才能對應到合適的 template),持續此步驟直到 `INDEX + 1` 到達我們特化的版本: ```cpp=15 template<int ...D> struct Helper<TABLE_SIZE, D...> { static constexpr std::array<int, TABLE_SIZE> table = { D... }; }; ``` 這是因為 compile 會優先配對已有 specialization 的 template 版本,第 17 行會在此 `struct`(注意為 `struct Helper<TABLE_SIZE, D...>` 這個 object,而非 `struct Helper`) 中宣告一個 `std::array` 並將之前的所有 `INDEX + 1` 放入,建立一個 `{0, 1, 4. .... 81}` 內容的 `table` 變數。 而因為我們呼叫的 `struct Helper<>` 一直繼承到 `struct Helper<TABLE_SIZE, D...>`,故也會擁有 `table` 變數,此 `table` 即為我們的目標,且以上行為皆在 compile time 內完成。 但當我要把上述程式應用到 raycaster 中時遇到許多問題: 1. 需要計算多個 table,需要為每個 table 都建立 template 與 struct 讓程式碼雜亂 2. 有些 table 的 type 不同 3. C++11 對於 compile time 的計算有許多限制並未放寬 因為 Makefile 內定義使用 C\+\+11 來編譯,我以盡量不改動為原則來修改程式碼 (C++14 以上可使用 `constexpr` 搭配 `for` 迴圈來產生 table), 以下概念來自 stack overflow 上的 [Xeo](https://stackoverflow.com/a/13315884/819272), [TemplateRex](https://stackoverflow.com/a/19023500) 及 [dyp](https://stackoverflow.com/a/19016627) 的答案。 先看底下程式碼: ```cpp template<unsigned... Is> struct seq{}; template<unsigned N, unsigned... Is> struct gen_seq : gen_seq<N-1, N-1, Is...>{}; template<unsigned... Is> struct gen_seq<0, Is...> : seq<Is...>{}; template<unsigned... Is> constexpr Table MagicFunction(seq<Is...>){ return {{ whichCategory(Is)... }}; } constexpr Table MagicFunction(){ return MagicFunction(gen_seq<128>{}); } ``` 與上方一開始介紹的方法很像,差別在於我們不需要使用成員變數來存參數,而是將參數儲存在 `struct seq` 的 **`Parameter Pack`** 當中,而當我們要使用這些參數時,呼叫 `whichCategory(Is)...` 會將 parameter pack 展開並對每個參數執行 `whichCategory`。 以下舉個例子,假設現在 parameter pack 中儲存參數 `Is...` 為 `{0, 1, 2, 3}`: ```cpp unsigned square(unsigned a) return a * a; template<unsigned... Is> constexpr Table MagicFunction(seq<Is...>){ return {{ square(Is)... }}; } ``` 當 `MagicFunction` 的參數 `seq<Is...>` 準備好時,`return` 會展開成以下: ```cpp return {{ square(0), square(1), square(2), square(3) }}; ``` :::warning 找不到相關文件說明此種用法... ::: 接著可以把 function 當成 parameter 傳入 template 中,就能解決需要計算不同 table 的問題: ```cpp template<typename Generator, std::size_t... Is> constexpr auto MagicFunction(Generator g, seq<Is...>) -> std::array<decltype(g(std::size_t{})), sizeof...(Is)> { return {{g(Is)...}}; } call MagicFunction(g, gen_seq<5>{}); ``` 上面程式碼將自定義 function `g` 傳入,並使用 Arrow operator 來回傳參數,這樣可以根據 function `g` 決定需要的參數,解決 type 不同的問題,[`sizeof...`](https://en.cppreference.com/w/cpp/language/sizeof...) 則用來取得 Parameter pack 中的**參數數量**。 :::info C++11 對 function declaration 引入新的語法: `auto` identifier `(` argument-declarations... `)` `->` return_type ::: C++14 提供了兩個神奇的 template class: [`std::index_sequence`](https://en.cppreference.com/w/cpp/utility/integer_sequence) 及 `std::make_index_sequence`,其中 `std::make_index_sequence<>` 等同於上方 `gen_seq<>`;而 `std::index_sequence<>` 則相當於上方的 `seq<>`,差別在於 `std::index_sequence<>` 內部已經處理完中間 recursive 的部分,直接回傳最後的 parameter pack。 雖然這些是 C++14 才有的功能,但能在 [Boost](https://gitlab.com/redistd/integer_seq/blob/master/integer_seq.h) 內找到 C\+\+11 版本的實作。 接著就是將上述功能實現: ```cpp template<class Function, std::size_t... Indices> constexpr auto make_array_helper(Function f, std::index_sequence<Indices...>) -> std::array<typename std::result_of<Function(std::size_t)>::type, sizeof...(Indices)> { return {{ f(Indices)... }}; } template<int N, class Function> constexpr auto make_array(Function f) -> std::array<typename std::result_of<Function(std::size_t)>::type, N> { return make_array_helper(f, std::make_index_sequence<N>{}); } ``` [`std::result_of<>::type`](https://en.cppreference.com/w/cpp/types/result_of) 與 [`decltype`](https://en.cppreference.com/w/cpp/language/decltype) 的用法差不多,這邊使用 `std::result_of<>` 來減少程式碼。要建立 table 時只需將 function `f` 以及 table 的 entry 數量 `N` 傳入 `make_array` 當中,就會回傳所需的 `std::array` 物件,注意到我們只需兩個 template function 以及所需 table 的計算 function 就好。 因為 table 改成 `std::array` 而非 c type array,故 `raycaster_fixed.cpp` 中的 `MulTan` 以及 `AbsTan` 參數也需一併修改。 要注意 C++11 中 `constexpr function` 的[限制](https://en.cppreference.com/w/cpp/language/constexpr): >the function body must be either deleted or defaulted or contain only the following: >* null statements (plain semicolons) >* static_assert declarations >* typedef declarations and alias declarations that do not >* define classes or enumerations >* using declarations >* using directives >* if the function is not a constructor, exactly one return statement 在 raycaster 中計算 Table 的方法都很單純,使用 conditional operator `?:` 都能解決。 最後只需將此產生 table 的程式碼在編譯時期產生,我將程式碼全部放入 Makefile 中,並在編譯時期透過 Makefile 產生 `raycaster_tables.h` 檔案並接著編譯此檔案。 以上實作已提交到 [Github](https://github.com/hankluo6/raycaster/commit/59e5eac2d235663d5f5302c82a39a5b303f47c4b) 上,並移除 `raycaster_tables.h` 檔案,可以透過 `make` 來觀看產生檔案的結果,==注意 `make` 前必須沒有 `raycaster_tables.h` 存在,可使用 `make clean` 移除==。 最後,可以將資料依照 `precalculator.cpp` 輸出成表格,並使用指令 `cmp raycaster_tables.h my_table.h` 比較兩者是否有差異。 :::info `raycaster_tables.h` 將內部含有 `<cmath>` 中 built in function 的 function 設定為 `constexpr`,但 `<cmath>` 定義的這些 function 卻不是 `constexpr`,原則上應該會造成編譯錯誤。但 `gcc` 能夠將某些 builtin function 視為 constexpr,故不會產生錯誤。 參考: * [Is gcc considering builtins of non-constant expression functions to be constant expressions](https://stackoverflow.com/questions/22182432/is-gcc-considering-builtins-of-non-constant-expression-functions-to-be-constant#comment44214059_27906899) * [Is it a conforming compiler extension to treat non-constexpr standard library functions as constexpr?](https://stackoverflow.com/questions/27744079/is-it-a-conforming-compiler-extension-to-treat-non-constexpr-standard-library-fu) * [Bug 49813 - [C++0x] sinh vs asinh vs constexpr](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=49813) ::: ## 輸出算繪過程的 frame rate 可以先找到程式中主要進行迴圈的部份,其紀錄迴圈的間隔時間就是 fps,SDL 提供了兩個高精度紀錄時間的函式: ```cpp Uint64 SDL_GetPerformanceCounter(void) int64 SDL_GetPerformanceFrequency(void) ``` 其中 `SDL_GetPerformanceCounter` 用系統內的 performance counter 紀錄次數,而 `SDL_GetPerformanceFrequency` 則紀錄了 1 秒內 counter 運行的次數。 ```cpp while (!isExiting) { ... const auto nextCounter = SDL_GetPerformanceCounter(); const auto seconds = (nextCounter - tickCounter) / static_cast<float>(tickFrequency); printf("%2.4f fps\n", 1 / seconds); ... } ``` 所以原程式內的 `seconds` 已是紀錄前次迴圈與這次迴圈運行的時間,其倒數就為 frame rate。 ###### tags: `linux2020`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully