朱雁丞
    • 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
    • Engagement control
    • 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 Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control 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
    6
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # 2020q3 render ###### tags: `sysprog` contributed by < `sciyen` > [repository](https://github.com/sciyen/raycaster) on github [Toc] # 遊戲方法 - 按 :arrow_up:/:arrow_down: 前進/後退 - 按 :arrow_left:/:arrow_right: 向左轉/右轉 - 按 `e` 舉槍 - 按 `g` 開啟 God Mode 使出透視眼 # 特殊函數筆記 1. `float modff( float x, float * intptr );` in `math.h` ([Microsoft docs](https://docs.microsoft.com/zh-tw/cpp/c-runtime-library/reference/modf-modff-modfl?view=msvc-160)) Splits a floating-point value into **fractional** and **integer** parts. - This function **returns the signed fractional portion of x**. There's *no error return*. - Example: ``` x = -14.87654321; /* Divide x into its fractional */ y = modf( x, &n ); /* and integer parts */ result: y = -0.876543, n = -14 ``` # 程式原理分析 ## Overview ### Map 從 `IsWall()` 中可以推敲出與地圖 `g_map` 有關的資訊: - 地圖 `g_map` 定義在 `raycaster_data.h` 中,為 `uint8_t` 1d array with 32*4 elements. - 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: 15 ```c=15 if (tileX < 0 || tileY < 0 || tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) { return true; } ``` - 可以知道當 `tileX` 小於 `0` 或超過 `MAP_X-1` 時,即算是出界,因此 X 軸邊界最大值為 `MAP_X` ,其值為 32 , Y axis 同理。 `raycaster_float.cpp` line: 18 ```c=18 return g_map[(tileX >> 3) + (tileY << (MAP_XS - 3))] & (1 << (8 - (tileX & 0x7))); ``` - 此段程式碼用於判斷座標 (`tileX`, `tileY`) 是否為障礙物,由於 `g_map` 中,每個 element 為 `uint8_t` ,因此每個 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。 ::: 修正後就會發現消失的牆壁回來了! ![](https://i.imgur.com/EcDoHm0.png) :::success #### 更有彈性的寫法: 若地圖改以其他變數型態表示時,可以這樣設定:假設用 `uint32_t` 由於 32 為 $2^5$ 因此只要設定 `MAP_ELEMENT_SIZE` 為 5 就可以不用修改 `IsWall()` 內部的程式碼。 ```c= #define MAP_ELEMENT_SIZE 5 // 2^5 = 32 #define MAP_X (uint8_t)(1 << MAP_XS) #define MAP_ELEMENT_MASK ((1 << MAP_ELEMENT_SIZE) - 1) ... const uint32_t g_map[] = { 0b00000000000000000000000000000000, ... } ... bool IsWall(...){ ... return g_map[(tileX >> MAP_ELEMENT_SIZE) + (tileY << (MAP_XS - MAP_ELEMENT_SIZE))] & (1 << (MAP_ELEMENT_MASK - (tileX & MAP_ELEMENT_MASK))); } ``` ::: ### Texture 在程式碼中, texture 以 `uint8_t` 的陣列儲存,而大小是 `64x64` 的方塊,因此 $(2^6)^2 = 2^{12}=4096$。 線索: `raycaster_data.h`中, `g_texture8` 的定義: ```c=32 const uint8_t LOOKUP_TBL g_texture8[4096]; ``` `renderer.cpp`中,對每個 texture 元素的存取: ```c=38 auto tv = g_texture8[(ty << 6) + tx]; ``` ### Coordinate System 與一般數學上熟悉的角度定義不同,在遊戲中定義視角方向為 Y 方向,向視野右邊角度為正,向視野左邊角度為負。 <center><img src="https://i.imgur.com/lYkLG6w.png" style="width: 300px"></center> <center>Fig. Gaming coordinate system</center> 一般來說,求 X 方向應該使用 $cos$ ,Y 方向使用 $sin$ 但在這邊會相反,可以在 `game.cpp` 中找到線索: `game.cpp` line: 8 ```c=10 playerX += 0.5f * m * sin(playerA) * seconds * 5.0f; playerY += 0.5f * m * cos(playerA) * seconds * 5.0f; ``` 此外,在 `Distance()` 中也有類似的實作,將 `tileStep` 定義為如下的 vector: <center><img src="https://i.imgur.com/5CM2HML.png" style="width: 300px"></center> <center>Fig. Gaming coordinate system</center> 可以發現這邊重新定義的 unit vector 是以 $\theta$ 與 Y 軸夾角的情況下,投影在原本 X-Y 軸座標系下的單位向量。 `game.cpp` line: 42 ```c=42 int tileStepX = 1; if (rayA > M_PI) { tileStepX = -1; } int tileStepY = 1; if (rayA > M_PI_2 && rayA < 3 * M_PI_2) { tileStepY = -1; } ``` # Raycasting Procedures #### Assumption: 1. 視角僅可能為畫面中間,如此一來計算就只需要計算 `SCREEN_WIDTH` 次 `Distance()` (只要在畫面水平中線掃一次) #### Procedures - Trace Frame(`renderer.cpp`) 計算出畫面 (frame) 上每個 pixel 的顏色 (`GetARGB`) ,實際上,由於畫面上下對稱,因此只計算畫面水平中線,再往上下方向用 `Trace()` 計算畫面中的障礙物高度。 - Trace(`raycaster_float.cpp`, `raycaster_fixed.cpp`) 計算撞到的 Wall 在目前視角下的障礙物資訊,包含 texture、該視角障礙物高度等資訊。 - Distance ### TraceFrame() 對整個畫面 render ,沿著 X 方向執行 `trace()` : <center><img src="https://i.imgur.com/r1F0Y3z.png" style="width: 300px"></center> <center>Fig. Frame tracing direction</center> 障礙物在畫面中分成兩種情況: - 障礙物高度低於畫面高度 <center><img src="https://i.imgur.com/CmzUBFX.png" style="width: 400px"></center> <center>Fig. Field Of View</center> - 障礙物高度超過畫面高度 <center><img src="https://i.imgur.com/MiRruGZ.png" style="width: 400px"></center> <center>Fig. Field Of View</center> 因此要畫出畫面中特定 X 座標的障礙物我們需要以下資訊: - screenY: 障礙物在畫面中最高位置的 y 像素 - textureY0: 障礙物開始 render 的 texture 起始位置(再來只要一直往正 Y 方向 render 就好) - textureStep: 每在畫面中移動一個像素時, texture 的 Y 座標的變動值。 ### Trace() #### ==Function Objective== 計算給定的螢幕 X 的位置,撞到的 Wall 在目前視角下的畫面 ( Y 座標、材質等資訊) - input: - screenX: 往此畫面 X 座標看過去,此函式將回傳該點 Y 軸成像。 - output: - screenY: 障礙物在視野中的 Y 軸高度(最高點)。 - textureNo: 障礙物的 ID。 - textureX: 看到的障礙物 texture X axis index(最高點)。 - textureY: 看到的障礙物 texture Y axis index(最高點)。 - textureStep: 隨著畫面 Y 軸往下移動一個像素,在 texture Y index 中移動的距離。 <center><img src="https://i.imgur.com/iduRPAm.png" style="width: 400px"></center> <center>Fig. View window</center> 其中,特定 `screenX` 相對於畫面中心的角度差 $\Delta\theta$ 可以表示為: $$ \Delta\theta = acrtan(\frac{ViewOffsetX}{View Window Distance}) = arctan(\frac{screenX-\frac{screenWidth}{2}}{\frac{screenWidth}{2}\frac{\tau_{max}}{2}}) $$ - 其中,$\tau_{max}$ 可以限制可視範圍,$screenX$ 為 0 時為最左側;$screenX$ 為 $\frac{screenWidth}{2}$ 時為最右側。 $$ \Delta\theta_{max}=acrtan(\frac{\pm2}{\tau_{max}}) \approx \pm51.8^{\circ} $$ - $D_v$: View Window Distance 為視野畫面 (ViewWindow) 與 player (Origin) 的距離 :::success #### 設定 Field Of View (FOV) 如果要讓視角的定義更直覺,應該設定 FOV 視角: ```c= #define FOV_X (M_PI / 2) ``` `raycaster_float.cpp` ```c=145 float deltaAngle = atanf(((int16_t) screenX - SCREEN_WIDTH / 2.0f) / (SCREEN_WIDTH / (2.0f * tanf(FOV_X / 2.0f)))); ``` 如此一來 $$ \Delta\theta_{max}=acrtan(tan(\pm\frac{FOV_X}{2})) = \pm\frac{FOV_X}{2} $$ $$ D_v = \frac{screenWidth}{2tan(\frac{FOV_X}{2})} $$ 重新定義的 FOV 如下圖: - 綠線為水平 (X) 方向視角極限 - 藍線為垂直 (Y) 方向視角極限 <center><img src="https://i.imgur.com/IJhs2Qf.png" style="width: 300px"></center> <center>Fig. Field Of View</center> 45 度: ![](https://i.imgur.com/gnlWQaK.png) 90 度: ![](https://i.imgur.com/W8iMpNK.png) 120 度: ![](https://i.imgur.com/n3SAv9I.png) ::: #### ==計算畫面中障礙物的高度== <center><img src="https://i.imgur.com/ytddUtx.png" style="width: 300px"></center> <center>Fig. Gaming coordinate system</center> 利用相似三角形,可以做以下推論 $$ \frac{\frac{screenY}{2}}{D_v}=\frac{\frac{WallHeight}{2}}{distance},\\ i.e. \ screenY=\frac{WallHeight \times D_v}{distance}=\frac{WallHeight \times screenWidth}{4 \times distance \times tan(\frac{FOV_X}{2})} $$ :::success 為配合前面 FOV 的定義,可以做如下改寫: 將原本程式碼中的 `INV_FACTOR` 重新定義為 ```c= #define WALL_HEIGHT 1.0f #define INV_FACTOR \ (float) (WALL_HEIGHT * SCREEN_WIDTH / (4.0f * tanf(FOV_X / 2))) ``` ::: #### ==texture rendering== 由於這個地圖中的障礙物都是 1x1 的方塊,要把 texture 貼到方塊上只要知道方塊上任意點的 x-y 座標,就可以計算出相應的 texture 。 求出 Wall 在視野內的高度 (`screenY`) 後,就得到視野內的方塊 x-y 資訊,這邊使用定點數表示 x-y 座標: - Tecture X 方向: 在這個遊戲中因為視野不會傾斜,因此 y 方向皆為垂直線條, texture 的 X 座標隨螢幕的 X 座標成比例關係: $$ x = TextureSize \times HitOffsetRatio $$ `raycaster_float.cpp` ```c=152 *textureX = (uint8_t)(256.0f * modff(hitOffset, &dum)); ``` **Note**: `textureSize` 為 $2^6=32$ , 2 bits fixed point,因此 $2^{6+2}=256$ `renderer.cpp` ```c=21 const auto tx = static_cast<int>(tc >> 2); ``` 先把界於 `0~1` 的 `hitOffset` 對應到 `uint8_t` 能夠表示的最大範圍後 (`textureX`) , 在 `renderer.cpp` 再轉換到 texture 相對應的 size 。因為 texture size 為 $2^6=64$,`uint8_t` size 為 $2^8$ ,因此需要右移 $(8-2)=2$ ,相當於使用 2 bits 定點數。 :::info 這邊 X 方向使用 2 bits 定點數即可,是因為視野不會傾斜,不會對成像造成明顯影響。 ::: - Texture Y 方向,使用 10 bits fixed point: 目標為找出畫面上特定 Y 座標點對應的 texture 座標,因此可以看作兩個座標空間的轉換,這邊記作 ${Screen Coordinate}\to{Texture Coordinate}$: 將 texture 看作一線性變換: $$ textureY^*=textureY_0+y\frac{TextureSize-2\times textureY_0}{WallHeight} $$ $textureY_0$: texture 的起始座標。 $TextureSize$: texture 的圖片大小。 $WallHeight$: 視野中牆壁的高度,也就是 $2\times screenY$。若牆高超過視野高度,則 $WallHeight=ScreenHeight$ $$ textureY_0=TextureSize\frac{(ProjectHeight-ScreenHeight)/2}{ProjectHeight} $$ $ProjectHeight$: 投影在成像平面上的牆壁高度。 這邊有兩種狀況: - 牆壁高度沒有超過視野高度: $WallHeight=2 \times screenY \leq ScreenHeight$ 由於牆壁會完全畫出來,因此 texture 從 0 開始畫,即 $textureY_0=0$。 $$ textureY^* = 0 + y \frac{TextureSize}{WallHeight} $$ $y$ 為畫面取樣時的 y 座標,其範圍應為 $0\leq y\lt WallHeight$。 - 牆壁高度超過視野高度: $2 \times screenY \geq ScreenHeight = WallHeight$ 從視野中只能看到部分牆壁,因此最高處從 $textureY_0$ 開始。 :::success #### 原程式碼中的運算缺失 ##### screenY overflow 可以發現,出現異常的地方總是在牆壁高度過高的時候(相較於左側視野範圍內,較低的牆壁高度總是沒有異常) ![](https://i.imgur.com/HvwrmJr.png) 在原本程式碼中,並未判斷 `screenY` 是否超過 `screenHeight` 也未對超過的情況做處理。此外,在 $screenY$ 的計算中,原本的程式碼是以 `uint8_t` 的整數去接收 `INV_FACTOR / distance` 因此若牆壁高度的計算結果超過 255 ,便會導致 $screenY$ overflow。 `raycaster_float.cpp` 原程式碼: ```c=151 *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; } } ``` ##### 鋸齒畫面 此外,由於原本的做法是用 `textureStep(ts)` 做累加,在 Y 方向累加的同時,浮點誤差也會跟著變大導致畫面下半部型成鋸齒狀: `renderer.cpp` 原程式碼: ```c=37 auto ty = static_cast<int>(to >> 10); auto tv = g_texture8[(ty << 6) + tx]; to += ts; ``` <center><img src="https://i.imgur.com/48cvuy1.png" style="width: 500px"></center> <center>Fig. Fixed point 產生的鋸齒畫面</center> #### 修正 利用前述的方式計算 `textureY` ,先在 `raycaster_float.cpp` 中判斷 $WallHeight$ ,若成像的高度大於畫面範圍 $ScreenHeight$,則再計算對應的 $textureY$ `raycaster_float.cpp` 改為: ```c=157 auto txs = 2.0f * INV_FACTOR / distance; if (txs > SCREEN_HEIGHT){ *screenY = SCREEN_HEIGHT / 2; *textureY = static_cast<uint16_t>(TEXTURE_SIZE * (txs - SCREEN_HEIGHT) / 2 / txs * (1 << 10)); } else *screenY = txs / 2; ``` `renderer.cpp` 改為: ```c=37 uint16_t ty = (tso + (((TEXTURE_SIZE / 2) << 10) - tso) * y / sso) >> 10; ``` 由於程式碼中使用 10 bits 定點數,因此 `textureY` 在運算開始前先乘 `(1 << 10)` ,並且在計算 $textureY^*$ 最後再右移 10。 <center><img src="https://i.imgur.com/CpyREFv.png" style="width: 500px"></center> <center>Fig. Fixed point 修正後的畫面</center> ::: #### ==消除魚眼== 當距離太近的時候,會產生如下的魚眼效果: ![](https://i.imgur.com/R0QVCdk.png) <center>Fig. Fisheye effect</center> 未校正前的影像是投影在「成像平面」上(見下圖),產生 $M$ 點,可以發現,相較於投影在「投影球面」上的 $P$ 點 - 投影在成像平面上有**更寬闊的視野範圍**,但是會造成**失真**,因為成像位置與觀測位置每點的距離都不太一樣,越靠近左右成像距離越長。 要修正魚眼效果要修正成像距離,使其成像在「投影球面」上,因此: $$ Distance=LineDistance\times tan(\Delta \theta) $$ <center><img src="https://i.imgur.com/gF04T1T.png" style="width: 400px"></center> <center>Fig. Fisheye effect compensation</center> :::info 在程式碼中, $LineDistance$ 指的是 **物體** 至 **玩家** 的距離,而在這邊修正魚眼指的是 **成像平面投影點** 至 **觀測點** 的距離,但是由於 $screenY$ 的 [**計算**](https://hackmd.io/SgFjqgnWSpOWUrJRK1-16w?both#%E8%A8%88%E7%AE%97%E7%95%AB%E9%9D%A2%E4%B8%AD%E9%9A%9C%E7%A4%99%E7%89%A9%E7%9A%84%E9%AB%98%E5%BA%A6) 是線性的,因此先乘和後乘 $tan()$ 是等效的;$Distance$ 以此類推。 ::: :::success #### 魚眼修正的加速 由魚眼校正的計算可以發現,每個 $\Delta \theta$ 、 $tan(\Delta\theta)$ 對於 $X$ 皆是 1 對 1 的對應關係,因此可以建立對應的 table 做查表,以節省計算。 ::: ## raycaster_float.cpp ### Distance() #### Function Objective Find the distance to obstacles from current player's position and facing direction (ray). - hitOffset: 用於計算 texture 的 X offset,為撞到 wall 的該點與 player 的距離,其方向依據撞到的是 X 方向或 Y 方向有關。 - hitDirection: 表示撞到的是 X 方向或 Y 方向。 為了判斷從 player 射出的 ray 最先碰撞到的障礙物,這邊先計算 X、Y 方向的 `startDelta` ,其意義為分別從 X 及 Y 方向出發,最先遇到的網格 (grid) 距離,接下來因為 grid 皆為單位大小,因此只要計算經過幾個 grid 就好。 整理在各個象限時的情況: 1. $0 \leq \theta < \frac{\pi}{2}$,$tan(\theta)>0$ <center><img src="https://i.imgur.com/S2qILq2.png" style="width: 300px"></center> $$ \left\{ \begin{array}{lr} startDeltaX=(1-offsetY)tan(\theta) \\ startDeltaY=\frac{1-offsetX}{tan(\theta)} \end{array} \right. $$ 2. $\frac{\pi}{2} \leq \theta < \pi$,$tan(\theta)<0$ <center><img src="https://i.imgur.com/lV9PFKf.png" style="width: 300px"></center> $$ \left\{ \begin{array}{lr} startDeltaX=offsetY \times (-tan(\theta)) \\ startDeltaY=\frac{1-offsetX}{tan(\theta)} \end{array} \right. $$ 3. $\pi \leq \theta < \frac{3\pi}{2}$,$tan(\theta)>0$ <center><img src="https://i.imgur.com/R2ckrO3.png" style="width: 300px"></center> $$ \left\{ \begin{array}{lr} startDeltaX=offsetY \times (-tan(\theta)) \\ startDeltaY=\frac{offsetX}{-tan(\theta)} \end{array} \right. $$ 4. $\frac{3\pi}{2} \leq \theta < 2\pi$,$tan(\theta)<0$ <center><img src="https://i.imgur.com/BhS8RH4.png" style="width: 300px"></center> $$ \left\{ \begin{array}{lr} startDeltaX=(1-offsetY)(-tan(\theta)) \\ startDeltaY=\frac{offsetX}{-tan(\theta)} \end{array} \right. $$ :::success #### Simplifying the calculation of startDelta 這邊發現一個很有趣的現象,如果將 $tan(\theta)$ 前的正負號提出來,恰好為 X、Y 象限的單位向量,因此可以將原本的計算改為 $$ \left\{ \begin{array}{lr} startDeltaX = vecX \times tan(\theta) \times correctY \\ startDeltaY = vecY \times \frac{1}{tan(\theta)} \times correctX \end{array} \right. $$ 修正項與 X、Y 象限單位向量的關係為 $(correctY, correctX)=(unitX, unitY)$ ,這邊 X、Y 恰好相反。 <center><img src="https://i.imgur.com/5CM2HML.png" style="width: 300px"></center> <center>Fig. Unit vector</center> 而 $(vecX, vecY)$ 則與象限有關,可以整理成下表 ||$0 \leq \theta < \pi$|$\pi \leq \theta < 2\pi$| |-|-|-| |$vecY$|$1-offsetX$|$offsetX$| |$unitX$|$1$|$-1$| ||$\frac{\pi}{2} \leq \theta < \frac{3\pi}{2}$|$0 \leq \theta < \frac{\pi}{2}$&&$\frac{3\pi}{2} \leq \theta < 2\pi$| |-|-|-| |$vecX$|$offsetY$|$1-offsetY$| |$unitY$|$-1$|$1$| 如此一來可以大幅簡化原本 `startDelta` 的計算,也減少許多 if-else 的判斷次數 修改過的 `raycaster_float.cpp`: ```c=46 float vecX = 1 - offsetY; // The case that 3pi/2 ~ pi/2 float vecY = 1 - offsetX; // The case that 0 ~ pi int tileStepX = 1; // The case that 0 ~ pi int tileStepY = 1; // The case that 3pi/2 ~ pi/2 // Generate directional unit vector according to player angle if (rayA > M_PI) { tileStepX = -1; vecY = (offsetX == 0) ? 1 : offsetX; } if (rayA > M_PI_2 && rayA < 3 * M_PI_2) { tileStepY = -1; vecX = (offsetY == 0) ? 1 : offsetY; } // Calculate the starting delta float startDeltaX = vecX * tan(rayA) * tileStepY; float startDeltaY = vecY / tan(rayA) * tileStepX; ``` 再來測量修改後的加速情形,以下資料取樣自進入遊戲後不動的情況下, 14.8 秒間共 74 筆數據 (每 0.2 秒一筆)的平均結果,時間計算方法請參考 [Performance Estimation](https://hackmd.io/SgFjqgnWSpOWUrJRK1-16w?both#Performance-Estimation) 實驗 [詳細資料](https://docs.google.com/spreadsheets/d/1ADTBi5f8Ap-Z2bWFIqjCS75Kltma4PXpWCXGmZTwI3s/edit#gid=0&range=A1:M81) |實驗|fixed point(ms)|float point(ms)|diff(ms)|FPS(frame/sec)| |:-:|:-:|:-:|:-:|:-:| |Old|0.308|0.344|0.036|323.410| |Old|0.293|0.330|0.037|407.674| |New|0.263|0.289|0.026|286.609| |New|0.285|0.325|0.040|401.003| 可以發現使用簡化後的 float point 平均來說比原本方法快一點,但是沒有很明顯。 ::: 接下來要找與障礙物的碰撞點,這邊用 $0 \leq \theta \leq \frac{\pi}{2}$ 舉例: <center><img src="https://i.imgur.com/phXx11b.png" style="width: 400px"></center> 以 $intercept$ 為起點 $$ \left\{ \begin{array}{lr} interceptX&=rayX + startDeltaX \\ interceptY&=rayY + startDeltaY \end{array} \right. $$ 發射出的向量會以相同的方向,但以不同的大小前進,因此分成以下兩種可能: - 撞到橫軸,即為綠色點,其 Y 座標為整數 $$ hitPos=(interceptX, tileY) \\ moveVec=(|tan(\theta)|sgn(unitX), 1\cdot sgn(unitY)) $$ - 撞到縱軸,即為紅色點,其 X 座標為整數 $$ hitPos=(tileX, interceptY) \\ moveVec=(1\cdot sgn(unitY), \frac{1}{|tan(\theta)|}sgn(unitX)) $$ 因此,所有的碰撞點可以表示成如下: $$ hitPos^*=hitPos + n \times moveVec, \quad n \in \mathbb{Z} $$ <center><img src="https://i.imgur.com/Ict6Ezp.png" style="width: 500px"></center> <center>Fig. Collision points</center> 如此一來只要重複比對是哪一個碰撞點最先撞到即可。 - 紅色: $tileY+1<interceptY$ 只要這個條件還滿足,表示下一個紅色仍然在綠色的範圍內,因此,下一個點仍然是紅色。 - 如果撞到牆壁一定是 Y 方向的面。 ```c=75 while (((tileStepY == 1 && (interceptY <= tileY + 1)) || (tileStepY == -1 && (interceptY >= tileY)))) { somethingDone = true; tileX += tileStepX; if (IsWall(tileX, interceptY)) { verticalHit = true; rayX = tileX + (tileStepX == -1 ? 1 : 0); rayY = interceptY; *hitOffset = interceptY; *hitDirection = true; break; } interceptY += stepY; } ``` - 綠色: $tileX+1<interceptX$ 只要這個條件還滿足,表示下一個綠色仍然在紅色的範圍內,因此,下一個點仍然是綠色。 - 如果撞到牆壁一定是 X 方向的面。 ```c=89 while (!verticalHit && ((tileStepX == 1 && (interceptX <= tileX + 1)) || (tileStepX == -1 && (interceptX >= tileX)))) { somethingDone = true; tileY += tileStepY; if (IsWall(interceptX, tileY)) { horizontalHit = true; rayX = interceptX; *hitOffset = interceptX; *hitDirection = 0; rayY = tileY + (tileStepY == -1 ? 1 : 0); break; } interceptX += stepX; } ``` 藉由重複以上兩個判斷,除非撞到牆壁,否則交替會一直延續。在這個地圖中一定會撞到某個點,因為在 `IsWall()` 中有設定邊界,撞到範圍外一定會停止。 為了防止形成無窮迴圈,這邊用一個變數 `somethingDone` 判斷射線是否仍在前進,否則就回傳 0 跳出迴圈。 # Fixed point implementation ## 定點數運算 ### 數字系統 #### angle 在 fixed point 實作中,是以 8 bits 表示一個 $\frac{\pi}{2}$,可以在 `precalculator.cpp` 中找到線索: `precalculator.cpp` line: 45 ```c=45 int16_t da = static_cast<int16_t>(deltaAngle / M_PI_2 * 256.0f); ``` 也就是說,新變數 `da` $$ da=deltaAngle \times \frac{256}{\frac{\pi}{2}} $$ 因此,若要判斷 `angle` 所在象限 (有多少個 $\frac{\pi}{2}$ ),只要取 `angle >> 8` 即可;而 `angle % 256` 則為佔 $\frac{\pi}{2}$ 的比例。例如: `raycaster_fixed.cpp` line:101 ```c=101 const uint8_t quarter = rayA >> 8; const uint8_t angle = rayA % 256; ``` 注意: 和一般數學慣例不一樣的是,這邊的 `quarter` 是從第 0 象限開始。 #### 角度表示範圍與誤差 數學中 $tan(\theta)$ 與 $cot(\theta)$ 存在極端值 (當 $tan(\pi/2)$ 時為無窮大),也就是會出現 `uint16_t` 無法表示的範圍。 ##### 極值 $tan(\theta)$ 的極值出現在最靠近 $\frac{\pi}{2} + n \pi$ 的地方,其中 $n$ 為整數。 ##### 誤差 由於電腦數值系統中,僅能以有限數量的位元儲存數字,因此對於像是 $tan()$ 這種無理數,僅能近似其數值,而近似結果與實際數字的差距就定義為誤差。 $$ Error(\theta_a) = tan_a(\theta_a) - tan(\theta) $$ - $\theta_a$ 為在該數值表示方法下近似的 $\theta$ 為離散值。 - $\tan_a()$ 為選用的 $tan()$ 近似算法 為離散的 one to one mapping - $\theta$ 及 $tan()$ 為連續值 <center><img src="https://i.imgur.com/cQRBi3K.png" style="width:500px"></center> <center>Fig: Error of tan</center> 由於 $tan(\theta)$ 的微分為 $\frac{1}{sec^2(\theta)}$ ,其在 $0\leq\theta<\frac{\pi}{2}$ 的範圍內為嚴格遞增函數,可以得到 $abs(E_L(\theta)) < E_H(\theta) < \Omega(\theta)$ ,並且, $\theta$ 越接近 $\frac{\pi}{2}$, $\Omega(\theta)$ 越大。 <center><img src="https://i.imgur.com/CGLBhAT.png" style="width:400px"></center> <center>Fig: 1/sec^2()</center> ##### 討論與比較 - 假設 $\theta$ 為 8 bits 定點數 - 極值: 在 $\theta$ 最接近 $\frac{\pi}{2}$ 時,也就是 $\theta=\frac{\pi}{2} \frac{255}{256}$ ,`tan` 有最大值 $tan(\frac{\pi}{2} \frac{255}{256}) = 162.937$。 - 最大誤差: $\tan(\frac{\pi}{2} \frac{255}{256}) - \tan(\frac{\pi}{2} \frac{254}{256}) = 81.489$。 - 假設 $\theta$ 為 10 bits 定點數 - 極值: 在 $\theta$ 最接近 $\frac{\pi}{2}$ 時,也就是 $\theta=\frac{\pi}{2} \frac{1023}{1024}$ ,`tan` 有最大值 $tan(\frac{\pi}{2} \frac{1023}{1024}) = 651.335$。 - 最大誤差: $\tan(\frac{\pi}{2} \frac{1023}{1024}) - \tan(\frac{\pi}{2} \frac{1022}{1024}) = 325.950$。 - 但是如果在 $\frac{\pi}{2} \frac{255}{256}$ 的誤差則為 $\tan(\frac{\pi}{2} \frac{1020}{1024}) - \tan(\frac{\pi}{2} \frac{1019}{1024}) = 32.595$ 可以發現,使用 10 bits fixed point 的約比 8 bits fixed point 少 $60.0\%$ 的誤差。 ### 無號乘法 `MulU()` 這是一個將 8 bits 整數 `v` 乘以 8 bits **無號**定點數 `f` 的運算,回傳的是四捨五入過後的 16 bits 無號整數。 ```c= uint16_t RayCasterFixed::MulU(uint8_t v, uint16_t f) { const uint8_t f_h = f >> 8; const uint8_t f_l = f % 256; const uint16_t hm = v * f_h; const uint16_t lm = v * f_l; return hm + (lm >> 8); } ``` Example: ``` v: 0000 0011 | f: 0000 0001 | 0000 1000 fh: 0000 0001 -> higher bits fl: 0000 1000 -> lower bits hm: 0000 0011 lm: 0001 1000 re: 0000 0011 ``` 若 `lm` 小於 256 表示 `v * f` 的小數部分不超過 1 無法進位,因此會在 `lm >> 8` 的時候捨去。 ### 有號乘法 `MulS()` 這是一個將 8 bits 整數 `v` 乘以 8 bits **有號**定點數 `f` 的運算,回傳的是四捨五入過後的 16 bits 有號整數。 ```c= int16_t RayCasterFixed::MulS(uint8_t v, int16_t f) { const uint16_t uf = MulU(v, static_cast<uint16_t>(ABS(f))); if (f < 0) { return ~uf; } return uf; } ``` 這邊如果 f 小於 0 ,最後會做一個 not 運算變成 `1 補數`。 :::warning Q: 為什麼不是用 `2 補數`? ::: ### `AbsTan()` 利用 $tan()$ 的對稱關係,可以用映射的方式得到不同象限的函數值。 <center><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Tan_proportional.svg/1200px-Tan_proportional.svg.png" style="width: 300px"></center> <center>Fig. function of tan (wikimedia)</center> 如此一來,可以做個歸納: (這邊的象限定義使用前述的 [數字系統](#%E6%95%B8%E5%AD%97%E7%B3%BB%E7%B5%B1),以第 0 象限作為開始) - 第 1 象限: 垂直水平映射 - 第 2 象限: 不變 - 第 3 象限: 垂直水平映射 但是,`AbsTan()` 只需要做垂直映射,因此: ```c= inline int16_t RayCasterFixed::AbsTan(uint8_t quarter, uint8_t angle, const uint16_t *lookupTable) { if (quarter & 1) { return LOOKUP16(lookupTable, INVERT(angle)); } return LOOKUP16(lookupTable, angle); } ``` :::info #### invert 這邊垂直映射是利用 ```c= #define INVERT(x) (uint8_t)((x ^ 255) + 1) ``` 也就是說,利用 $a + b = 256$ 的關係,找出對稱的 index ``` 000 -> 000 001 -> 255 254 -> 002 255 -> 001 ``` ::: ### 乘 `MulTan()` 這個 function 用查表的方式快速計算 $value \times tan(angle)$ 或是 $value \times cot(angle)$ 由 $tan()$ 與 $cot()$ 的關係: $$ tan(-\theta+\frac{\pi}{2})=\frac{sin(-\theta+\frac{\pi}{2})}{cos(-\theta+\frac{\pi}{2})}=\frac{-cos(-\theta)}{sin(-\theta)}=cot(\theta) $$ 可以發現 $tan()$ 和 $cot()$ 的關係其實是將 $tan()$ 取垂直映射後,右(左)移 $\frac{\pi}{2}$: <center><img src="https://mathbitsnotebook.com/Algebra2/TrigGraphs/otherg97.gif" style="width: 300px"></center> <center>Fig. function of cot (https://mathbitsnotebook.com/Algebra2/TrigGraphs/TGtanSecCscCot.html)</center> 而實際上,可以藉由對 $\frac{\pi}{4}$ 映射得到。 如此一來,可以做個歸納: (這邊的象限 `quarter` 定義使用前述的 [數字系統](#%E6%95%B8%E5%AD%97%E7%B3%BB%E7%B5%B1),以第 0 象限作為開始) - 需要左右映射的: - $tan()$: 1、3 象限 - $cot()$: 0、2 象限 - 需要上下映射的 (負號): - $tan()$: 1、3 象限 - $cot()$: 1、3 象限 也就是 ```c= uint8_t signedValue = value; if (inverse) { if (value == 0) { if (quarter % 2 == 1) { return -AbsTan(quarter, angle, lookupTable); // cot 上下映射 } return AbsTan(quarter, angle, lookupTable); } signedValue = INVERT(value); } if (signedValue == 0) { return 0; } if (quarter % 2 == 1) { return -MulU(signedValue, LOOKUP16(lookupTable, INVERT(angle))); // cot 上下映射 } return MulU(signedValue, LOOKUP16(lookupTable, angle)); ``` ### `CalculateDistance()` 這邊的 `CalculateDistance()` 與 `raycaster_float.cpp` 中的 `Distance()` 很像,不一樣的是 1. `CalculateDistance()` 中的 `tan()` 計算改以查表的方式進行。 2. 對於 `angle` 等於 0 的特例做了最佳化。 3. 使用 `goto` 的技巧簡化在 `angle == 0` 與 `angle != 0` 的情況中,計算 texture 的重複性程式碼。 # New Features ## Performance Estimation 利用 SDL2.0 的 API ,可以計算 High resolution 的 performance. - `SDL_GetPerformanceCounter()` Returns the current counter value. - `SDL_GetPerformanceFrequency()` Returns a platform-specific count per second. 為了分別計算 fixed point performance、float point performance、FPS 這三種指標,利用 counter 為其計時: `main.cpp`: ```c=125 const auto renderFloatTickStart = SDL_GetPerformanceCounter(); floatRenderer.TraceFrame(&game, floatBuffer); const auto renderFloatTickEnd = SDL_GetPerformanceCounter(); const auto floatRenderSeconds = (renderFloatTickEnd - renderFloatTickStart) / static_cast<float>(tickFrequency); ``` #### Exponential Filter 由於計算速度很快,容易產生許多高頻雜訊,所以引入 [Exponential Filter](https://gregstanleyandassociates.com/whitepapers/FaultDiagnosis/Filtering/Exponential-Filter/exponential-filter.htm) $$ y(k) = a*y(k-1) + (1-a)*x(k) $$ where - $x(k)$ is the raw input at time step $k$ - $y(k)$ is the filtered output at time step $k$ - $a$ is called the `smoothing constant`, normally between 0.8 ~ 0.99 這邊 $T$ 我們用估計的 frame update rate ,大約是 0.00357 (s), `smoothing factor` $\tau$ 選擇 0.95 $$ a = e^{\frac{-T}{\tau}} \quad or \quad \tau = \frac{-T}{log(a)} $$ $\tau$ is the resulting `filter time constant` 大約為 0.16 (s) ,表示假設為 step input ,大概耗費 0.16 秒,資料會成長 $1/e \approx 36.8\%$ `main.cpp`: ```c=67 void printPerformanceInfo(float floatRenderSec, float fixedRenderSec, float overallSec) { const float smoothFactor = 0.95f; static auto lastPrintTick = SDL_GetPerformanceCounter(); static float floatRC = floatRenderSec; static float fixedRC = fixedRenderSec; static float overRC = overallSec; auto currentTick = SDL_GetPerformanceCounter(); const static auto tickFrequency = SDL_GetPerformanceFrequency(); floatRC = smoothFactor * floatRC + (1 - smoothFactor) * floatRenderSec; fixedRC = smoothFactor * fixedRC + (1 - smoothFactor) * fixedRenderSec; overRC = smoothFactor * overRC + (1 - smoothFactor) * overallSec; if ((currentTick - lastPrintTick) / static_cast<float>(tickFrequency) > 0.2f) { printf("fixed: %.6f(s), float: %.6f(s), FPS: %.6f(s)\n", fixedRC, floatRC, 1 / overRC); lastPrintTick = currentTick; } } ``` ## Multi obstacles ### 修改地圖儲存方式 `g_map32` 原本的地圖一個 position 只有兩種情況,因此只需要一個 bit 即可表示: - It is a wall: 以 `1` 表示 - It is not a wall: 以 `0` 表示 若要儲存不同的 wall ,表示一個 position 有多種可能,需要在 `g_map` 中拓展一個 position 的儲存空間,由於原本的地圖是以 `uint8_t` 若將型態改為 `uint32_t` 即可以將一個 position 以 4 bits 表示,共可以表示 16 種牆壁,而不改變原本地圖 X-Y 座標的定義。 以下為將 8 bits 地圖轉換為 32 bits 地圖的轉換工具: ```c= uint32_t out_map[sizeof(g_map)] = {0}; for (int i = 0; i < sizeof(g_map); i++) { uint8_t in = g_map[i]; for (int j = 0; j < 8; j++) { out_map[i] |= (in & (1 << j)) << (4 * j - j); } } ``` 該程式在每個 g_map 後面插入三個 0: ``` 0b 0 1 1 1 1 0 1 0 -> 表示 8 個牆壁,每個牆壁 1 bit 0b00000001000100010001000000010000 -> 表示 8 個牆壁,每個牆壁 4 bit ``` ### 新地圖的牆壁判斷 `IsWall()` 接下來,關係會變得稍微複雜,首先定義以下常數 - `MAP_XS`: 地圖大小的指數 (假設地圖寬度為 32 ,則 `MAP_XS` 為 5 ,因為 $32=2^5$) - `MAP_ELEMENT_WS`: Element 的意義為,儲存地圖的單位型態,例如: - `uint8_t` 為 8 bits 因此 `MAP_ELEMENT_WS` 為 3 ($8=2^3$); - `uint32_t` 為 32 bits 因此 `MAP_ELEMENT_WS` 為 5 ($32=2^5$)。 - `OBSTACLE_SIZE`: 只用來儲存一個 obstacle (wall) 使用的 bits 數量,例如使用 4 bits 則會有 $2^4=16$ 種 wall 的可能。 接下來的常數是由以上使用者定義的常數衍生出來的, - `MAP_X`: 地圖大小,為 $2^{MAP\_WS}$ - `MAP_ELEMENT_SIZE`: 儲存地圖的單位變數 bits 數量,為 $2^{MAP\_ELEMENT\_WS}$ - `OBSTACLES_PER_ELEMENT`: 每個單位變數能夠儲存的 obstacle 數量,為 $\frac{MAP\_ELEMENT\_SIZE}{OBSTACLE\_SIZE}$ - `ELEMENTS_PER_ROW`: 儲存每個橫軸 (X軸) 所需要的變數數量,為 $\frac{MAP\_X}{OBSTACLES\_PER\_ELEMENT}$ - `OBSTACLE_MASK`: 單一個 obstacle 的 bit mask ,用來取得單一 obstacle 的 filter,為 $2^{OBSTACLE\_SIZE}-1$ 假設 ``` MAP_XS := 5 // map is 32 x 32 wide MAP_ELEMENT_WS := 5 // using uint32_t OBSTACLE_SIZE := 4 // storing 1 wall with 4 bits - then we can get: MAP_X = 32 MAP_ELEMENT_SIZE = 32 OBSTACLES_PER_ELEMENT = 8 ELEMENTS_PER_ROW = 4 OBSTACLE_MASK = 0b1111 ``` 將 `IsWall()` 中做判斷的程式碼改為如下 ```c=18 return (g_map32[(tileX / OBSTACLES_PER_ELEMENT) + (tileY * ELEMENTS_PER_ROW)] >> ((OBSTACLES_PER_ELEMENT - tileX % OBSTACLES_PER_ELEMENT) * OBSTACLE_SIZE)) & OBSTACLE_MASK; ``` 首先要取得地圖中紀錄給定座標的變數,因此計算 index: $$ index=tileY \times ELEMENTS\_PER\_ROW + \frac{tileX}{OBSTACLES\_PER\_ELEMENT} $$ 接下來要從每個變數中取得對應於目前 tileX 中相對應的 bits ,需注意的是,由於 LSB 對應於 tileX+ 方向,因此 ``` 0 1 2 3 4 5 6 7 // tileX % OBSTACLES_PER_ELEMENT = 4 0b00000001000100010001000000010000 // g_map32 假設 tileX 為 4 因此我們需要右移 3 格 // 7 - 4 0b00000000000000000001000100010001 最後加上 obstacle bits mask: 0b1111 0b00000000000000000000000000000001 即可得到 ``` ### 不同碰撞情況的表示方式 原本的程式碼利用 `hitDirection` 表示 ray 撞擊的方向,假如是在 vertical 方向則標示為 1 ;反之為 0 。 在擴增地圖版本中,為了同時傳達不同的 obstacles 與 `hitDirection` ,將 `hitDirection` 重新定義: - `hitDirection` / 2 表示 texture id - `hitDirection` 如為奇數,則為 vertical hit 如此一來就可以保留原本的實作,並且與舊版相容。 實際的改寫包含: `raycaster_float.cpp`: line 85,將 hitDirection 指定為 `IsWall()` 的回傳值,其意義為撞到的 obstacle ID,並且由於是 vertical hit ,根據以上規則,將 `hitDirection` * 2 + 1 ```c= if (*hitDirection = IsWall(tileX, interceptY)) { verticalHit = true; rayX = tileX + (tileStepX == -1 ? 1 : 0); rayY = interceptY; *hitOffset = interceptY; // Use odd number to indicate different hit direction *hitDirection = (*hitDirection - 1) * 2 + 1; break; } ``` `raycaster_float.cpp`: line 100,將 hitDirection 指定為 `IsWall()` 的回傳值,其意義為撞到的 obstacle ID,並且由於是 horizontal hit ,根據以上規則,將 `hitDirection` * 2 ```c= if (*hitDirection = IsWall(interceptX, tileY)) { horizontalHit = true; rayX = interceptX; *hitOffset = interceptX; rayY = tileY + (tileStepY == -1 ? 1 : 0); *hitDirection = (*hitDirection - 1) * 2; break; } ``` `renderer.cpp`: line 43,將 **dark wall** 的定義改為判斷奇數: ```c= if (tn % 2 == 1 && tv > 0) { // dark wall tv >>= 1; } ``` :::success 這邊為了保留原本的 texture 相容性,設定 `textureNo` 為 0 時為 gray scale 模式,因此 ::: ### 多種 texture #### 32 bits color encode to 16 bits color 為了在有限的記憶體空間中表示彩色,我將一個 `uint16_t` 分成幾個 part: - A (Apparent 透明度): 1 bits - R (Red): 5 bits - G (Green): 5 bits - B (Blue): 5 bits ```c= #define CHANNEL_BITS 5 #define CHANNEL_MASK (((1 << CHANNEL_BITS) - 1) << (8 - CHANNEL_BITS)) #define R_OFFSET (CHANNEL_BITS * 2 - (8 - CHANNEL_BITS)) #define G_OFFSET (CHANNEL_BITS * 1 - (8 - CHANNEL_BITS)) #define B_OFFSET (8 - CHANNEL_BITS) ``` - `CHANNEL_BITS`: 用來儲存單一顏色的 bits 數量 - `CHANNEL_MASK`: 由於要從原本的 8 bits 顏色轉換為 5 bits ,因此要犧牲一些精度,這邊只取 MSB 的 5 個 bits。 - `R_OFFSET`、`G_OFFSET`、`B_OFFSET`: 分別計算不同顏色 channel 所需的位移量 因此新的 16 bits 顏色就可以這樣取得: ```c= uint16_t out = ((r & CHANNEL_MASK) << R_OFFSET) + ((g & CHANNEL_MASK) << G_OFFSET) + ((b & CHANNEL_MASK) >> B_OFFSET); ``` ``` R: 0b11111000 G: 0b11111000 B: 0b11111000 0b0111111111111111 ``` 這邊的色彩轉換是用我之前寫的一個 bmp 圖片讀取工具做的 ```c= if (openBMP(&bmp, filename)) { int width = getWidth(&bmp); int height = getHeight(&bmp); unsigned char r, g, b; for (int y = height - 1; y >= 0; y--) { for (int x = 0; x < width; x++) { // Obtain the rgb value of a pixel at (x, y) getPixelData(&bmp, x, y, &r, &g, &b); uint16_t out = ((r & CHANNEL_MASK) << R_OFFSET) + ((g & CHANNEL_MASK) << G_OFFSET) + ((b & CHANNEL_MASK) >> B_OFFSET); printf("%d, ", out); } } // Release bmp memory freeBmp(&bmp); } else { printf("Fail to open bmp file"); return 0; } ``` #### 16 bits color decode to 32 bits color 我在 `raycaster.h` 中新增以下定義用於計算 decode 時所需的各個 mask 及 offset : ```c=29 // Number of bits to representing a single color channel #define TEXTURE_BITS_COLOR 5 #define TEXTURE_COLOR_MASK ((1 << TEXTURE_BITS_COLOR) - 1) #define TEXTURE_R_MASK (TEXTURE_COLOR_MASK << (TEXTURE_BITS_COLOR * 2)) #define TEXTURE_G_MASK (TEXTURE_COLOR_MASK << (TEXTURE_BITS_COLOR * 1)) #define TEXTURE_B_MASK (TEXTURE_COLOR_MASK << (TEXTURE_BITS_COLOR * 0)) #define TEXTURE_R_OFFSET \ (8 * 2 + (8 - TEXTURE_BITS_COLOR) - TEXTURE_BITS_COLOR * 2) #define TEXTURE_G_OFFSET \ (8 * 1 + (8 - TEXTURE_BITS_COLOR) - TEXTURE_BITS_COLOR * 1) #define TEXTURE_B_OFFSET \ (8 * 0 + (8 - TEXTURE_BITS_COLOR) - TEXTURE_BITS_COLOR * 0) ``` 將 `uint16_t` 的顏色轉換為 `uint32_t` ,`renderer.h`: line 16 ```c=16 inline static uint32_t GetARGB_color(uint16_t color) { return ((color & TEXTURE_R_MASK) << TEXTURE_R_OFFSET) + ((color & TEXTURE_G_MASK) << TEXTURE_G_OFFSET) + ((color & TEXTURE_B_MASK) << TEXTURE_B_OFFSET); } ``` #### texture mapping 為了讓原本的 gray scale texture 可以相容,因此區分兩種 texture rendering method: - Gray Scale: 使用 `GetARGB()` decode - Colored: 使用 `GetARGB_color()` decode 記錄不同 texture 的 texture list: `renderer.h` ```c=23 const uint8_t *g_texture_gray[NUMBER_OF_GRAY_TEXTURE] = {g_texture8}; const uint16_t *g_texture_color[2] = {g_texture8_computer, g_texture8_cat}; ``` 根據 [前述](#%E4%B8%8D%E5%90%8C%E7%A2%B0%E6%92%9E%E6%83%85%E6%B3%81%E7%9A%84%E8%A1%A8%E7%A4%BA%E6%96%B9%E5%BC%8F) `textureNo` 的定義,依據 ray 撞擊的 obstacle 種類 (`tn / 2`) 可以判斷出要以 gray scale 或是 colored 方式選擇 texture 及 render: `renderer.cpp`: ```c=40 uint16_t tv; if (tn / 2 < NUMBER_OF_GRAY_TEXTURE) { // gray scale texture tv = (g_texture_gray[tn / 2])[(ty << 6) + tx]; *lb = GetARGB(tv); } else { // colored texture tv = (g_texture_color[tn / 2 - 1])[(ty << 6) + tx]; *lb = GetARGB_color(tv); } ``` 原本程式碼的設計是當遇到 vertical hit 的 obstacles 的時候,會將原本的 texture 以亮度變暗的方式做 render ,但如果是 colored 的 textu部則不能直接右移,因為不同的 color channel 會互相影響,因此在平移之前加一個 `0xFEFEFE` 的 mask ,確保 LSB 都是 0 再做平移。 `renderer.cpp`: ```c=51 // View with different direction if (tn % 2 == 1 && *lb > 0) { // dark wall *lb = (*lb & 0xFEFEFE) >> 1; } ``` #### 效果比較 - 8 bits texture 效果 (Ax2, Rx2, Gx2, Bx2),色彩深度: 64 <table> <tr><th>原圖 (64x64 bmp)</th><th>8 bits texture</th><tr> <tㄉˇ <td><img src="https://i.imgur.com/2QGQoq3.png" style="width: 300px"></td> <td><img src="https://i.imgur.com/UMSdYGy.png" style="width: 400px"></td></tr> </table> - 16 bits texture 效果 (Ax2, Rx2, Gx2, Bx2),色彩深度: 32768 <table> <tr><th>原圖 (64x64 bmp)</th><th>16 bits texture</th><tr> <tr> <td><img src="https://i.imgur.com/2QGQoq3.png" style="width: 300px"></td> <td><img src="https://i.imgur.com/HzSe3KS.png" style="width: 400px"></td></tr> </table> ## 遊戲體驗加強 再來我加入一些遊戲的元素,例如持槍、準心等等。 我在 `Renderer` 中新增了一個 function `RenderGame()` 負責所有非 raycasting 的 render. ### 顯示槍枝 - Gun Side View: 在 player 未舉槍之前,槍枝以 side view 方式呈現,並且隨著 player 行動,槍枝會跟著步伐晃動。 - 準心晃動: 因為槍枝晃動所以準心也會跟著變大變小。 - Gun Center View: 舉槍後,視野縮小,準心也縮小。 #### Gun Side view 我將 color 的 MSB 作為圖片透明控制,因此在 render 的時候檢查 color 的 MSB 是 0 才需要 override 該像素的 color buffer. ```c=79 // rendering hand and gun uint32_t *lb = fb + (SCREEN_HEIGHT - TEXTURE_GUN_SIDE_HEIGHT + (uint8_t) offset) * SCREEN_WIDTH + (SCREEN_WIDTH - TEXTURE_GUN_SIDE_WIDTH); for (int j = 0; j < TEXTURE_GUN_SIDE_HEIGHT - (uint8_t) offset; j++) { for (int i = 0; i < TEXTURE_GUN_SIDE_WIDTH; i++) { auto tv = g_texture_gunside[j * TEXTURE_GUN_SIDE_WIDTH + i]; if ((tv & 0x8000) == 0) *lb = GetARGB_color(tv); lb++; } lb += SCREEN_WIDTH - TEXTURE_GUN_SIDE_WIDTH; } ``` #### Arming point 準心大小可以從 `game.h` 設定 ```c=5 #define ARM_POINT_LEN 10 #define ARM_POINT_RAD 10 #define ARM_POINT_COLOR 0x00FFFFFF ``` `renderer.cpp` 把準心畫出來 ```c=96 // rendering aiming point lb = fb + (SCREEN_HEIGHT / 2) * SCREEN_WIDTH + (SCREEN_WIDTH / 2); for (int l = 0; l < ARM_POINT_LEN; l++) { uint8_t len = offset + l + ARM_POINT_RAD; *(lb - len * SCREEN_WIDTH) = ARM_POINT_COLOR; *(lb + len * SCREEN_WIDTH) = ARM_POINT_COLOR; *(lb - len) = ARM_POINT_COLOR; *(lb + len) = ARM_POINT_COLOR; } ``` ![](https://i.imgur.com/1h4PRzn.png) #### Moving 為了呈現人物在走動時晃動的效果,讓 `Game` 多紀錄一個參數 `moving` ,表示該 time step 時人物的移動距離,然後再用一個週期函數 (這邊用 sin ) 讓 offset 持續震盪,最後,為了讓 offset 在人物沒有移動時逐漸回到原點,將 offset 乘上一個小於 0 的值。 ```c=71 static float time = 0, offset = 0; if (g->moving > 0) { time += 0.02f; offset = (10 * (sin(time) + 1)); } else { offset *= 0.99f; } ``` #### Gun Center view 增加舉槍動作,當按下 `e` 時,可以切換為舉槍姿勢, 我把 player 的姿態記錄在 `Game` 裡,定義 enum 為: ```c=9 enum PlayerPose {POSE_STAND, POSE_SQUAT}; ``` 並且在 `Game` 中加入 ```c=14 PlayerPose pose; ``` 取得鍵盤事件,我將 `main.cpp` 中的 `ProcessEvent()` 增加了一個 `e` 的判斷: ```c=34 static bool ProcessEvent(const SDL_Event &event, int *moveDirection, int *rotateDirection, Game *game) { ... case SDLK_e: game->pose = p ? POSE_SQUAT : POSE_STAND; ... } ``` 修改槍枝的 render ,若為蹲下的 pose (`POSE_SQUAT`) ,則畫出另一個槍枝的 texture: ```c=79 // rendering hand and gun if (g->pose == POSE_SQUAT) { const int sx = SCREEN_WIDTH / 2 - TEXTURE_GUN_CENTER_WIDTH / 2 + 1; const int sy = SCREEN_HEIGHT - TEXTURE_GUN_CENTER_HEIGHT; uint32_t *lb = fb + sy * SCREEN_WIDTH + sx; for (int j = 0; j < TEXTURE_GUN_CENTER_HEIGHT; j++) { for (int i = 0; i < TEXTURE_GUN_CENTER_WIDTH; i++) { auto tv = g_texture_gun_center[j * TEXTURE_GUN_CENTER_WIDTH + i]; if ((tv & 0x8000) == 0) *lb = GetARGB_color(tv); lb++; } lb += SCREEN_WIDTH - TEXTURE_GUN_SIDE_WIDTH; } } else { uint32_t *lb = fb + (SCREEN_HEIGHT - TEXTURE_GUN_SIDE_HEIGHT + (uint8_t) offset) * SCREEN_WIDTH + (SCREEN_WIDTH - TEXTURE_GUN_SIDE_WIDTH); for (int j = 0; j < TEXTURE_GUN_SIDE_HEIGHT - (uint8_t) offset; j++) { for (int i = 0; i < TEXTURE_GUN_SIDE_WIDTH; i++) { auto tv = g_texture_gun_side[j * TEXTURE_GUN_SIDE_WIDTH + i]; if ((tv & 0x8000) == 0) *lb = GetARGB_color(tv); lb++; } lb += SCREEN_WIDTH - TEXTURE_GUN_SIDE_WIDTH; } } ``` 效果展示: ![](https://i.imgur.com/WMaZHxW.png) ### 碰撞 在更新 player 位置之前,先檢查會不會撞到牆,如果會撞到的話讓 player 往反方向反彈。 `game.cpp`: ```c=13 float try_move_x = 0.5f * m * sin(playerA) * seconds * 5.0f; float try_move_y = 0.5f * m * cos(playerA) * seconds * 5.0f; int x = playerX + try_move_x; int y = playerY + try_move_y; if (((g_map32[(x / OBSTACLES_PER_ELEMENT) + (y * ELEMENTS_PER_ROW)] >> ((OBSTACLES_PER_ELEMENT - x % OBSTACLES_PER_ELEMENT) * OBSTACLE_SIZE)) & OBSTACLE_MASK) == 0) { playerX += try_move_x; playerY += try_move_y; } else { playerX -= try_move_x; playerY -= try_move_y; } ``` ## 透明牆壁 ### Allow the ray keep going 能夠看到障礙物後面的物體,表示光線能夠穿透障礙物,在這個專題的意思就是,在 raycasting 的時候, ray 不能夠被牆壁阻擋。於是乎我把腦筋動到 `Distance()` 上,如何重複呼叫 `Distance()` 是關鍵的問題。 一開始我多加一個參數叫做 `keepGoing` 來判斷是否是上次的 ray: `raycaster_float.cpp` ```c=26 float RayCasterFloat::Distance(float playerX, // In, Player location X float playerY, // In, Player location Y float rayA, // In, Player location Angle float *hitOffset, // Out, int *hitDirection, // Out, bool keepGoint) // In { ... } ``` 但是這樣我發現必須從 `Trace()` 就傳入 `keepGoing` 的參數,因此連 `Trace()` 都要修改函數原型,導致 `raycaster.h` 中 `Raycaster` 的 virtual function `Trace()` 要連帶修改,最後連毫無關聯的 `raycaster_fixed.cpp` 也要修改,因此這樣不是一個好的辦法。 `raycaster.h` ```c=73 virtual void Trace(uint16_t screenX, uint8_t *screenY, uint8_t *textureNo, uint8_t *textureX, uint16_t *textureY, uint16_t *textureStep) = 0; ``` 但後來我發現,如果在同一個地方發出 ray ,則 x 座標應該是一樣的,在 `Distance()` 內部判斷就可以避免修改 `Raycaster` 的原型。 `raycaster_float.cpp` ```c=135 void RayCasterFloat::Trace(uint16_t screenX, uint8_t *screenY, uint8_t *textureNo, uint8_t *textureX, uint16_t *textureY, uint16_t *textureStep) { static uint16_t previousX = UINT16_MAX; bool keepGoing = false; if (screenX == previousX) keepGoing = true; previousX = screenX; ... float lineDistance = Distance(_playerX, _playerY, _playerA + deltaAngle, &hitOffset, &hitDirection, keepGoing); ... } ``` 我用了一個 `static` 的變數去紀錄上一次 X 座標的值,當這次與前次不一樣表示 ray 應該 keepGoing ,並且把這個資訊傳給 `RaycasterFloat` 的 private function `Distance()` 在 `Distance()` 中,我發現若要沿用上一次的結果,大部分參數都不需要重新計算,唯一要注意的是 $(tileX, inceptionY)$ 與 $(inceptionX, tileY)$ 的資料更新,因此,我將大部分參數設定為 `static` 並用 `keepGoing` 決定要不要計算初始值 $(startDeltaX, startDeltaY)$ 設定為 `static` 的參數: ```c=44 static float rayX = playerX; static float rayY = playerY; static float tileX = 0, tileY = 0, offsetX = 0, offsetY = 0, startDeltaX = 0, startDeltaY = 0, interceptX = 0, interceptY = 0; static int tileStepX = 0, tileStepY = 0; ``` 將初始 `startDelta` 、 `inception` 、 `tile` 的計算放進 `keepGoing` 檢查: ```c=50 if (!keepGoing) { rayX = playerX; rayY = playerY; offsetX = modff(rayX, &tileX); offsetY = modff(rayY, &tileY); float vecX = 1 - offsetY; // The case that 3pi/2 ~ pi/2 float vecY = 1 - offsetX; // The case that 0 ~ pi tileStepX = 1; // The case that 0 ~ pi tileStepY = 1; // The case that 3pi/2 ~ pi/2 // Generate directional unit vector according to player angle if (rayA > M_PI) { tileStepX = -1; vecY = (offsetX == 0) ? 1 : offsetX; } if (rayA > M_PI_2 && rayA < 3 * M_PI_2) { tileStepY = -1; vecX = (offsetY == 0) ? 1 : offsetY; } // Calculate the starting delta startDeltaX = vecX * tan(rayA) * tileStepY; startDeltaY = vecY / tan(rayA) * tileStepX; interceptX = rayX + startDeltaX; interceptY = rayY + startDeltaY; } ``` 最後我發現 inception 需要一個補償,但我不確定原因為何,若沒有補償會出現下面的現象,穿透牆壁後, texture 的位置錯開了。 ![](https://i.imgur.com/dRjBJrw.png) 我在 `Distance()` 的最後加入下面的補償,以 vertical hit 的 case 看得話,我覺得原因應該是在判斷碰撞的 while 迴圈中,可以發現只有 tileX 被往前推進,而在判斷牆壁之後就直接 break 了,因此 inceptionY 會少一次推進,而 horizontal hit 也是如此。 ```c=123 if (verticalHit) interceptY += stepY; else interceptX += stepX; ``` vertical hit 的計算: ```c=86 while (((tileStepY == 1 && (interceptY <= tileY + 1)) || (tileStepY == -1 && (interceptY >= tileY)))) { somethingDone = true; tileX += tileStepX; // 這邊 X 軸被推進 if (*hitDirection = IsWall(tileX, interceptY)) { verticalHit = true; rayX = tileX + (tileStepX == -1 ? 1 : 0); rayY = interceptY; *hitOffset = interceptY; // Use odd number to indicate different hit direction *hitDirection = (*hitDirection - 1) * 2 + 1; break; // 遇到 break 之後 } interceptY += stepY; // 這個不會執行到 } ``` ### Rendering apparent walls 現在我們已經可以偵測牆壁後面的牆壁了,只要在同一個 X 軸下重複呼叫 `RaycasterFloat` 的 `Trace()` 即可,他會回傳牆壁之後的牆壁的計算結果。 接下來為了簡化程式碼,我在 `renderer.cpp` 中重新設計了一個 general 版本的 `TraceFrame()` Helper,可以簡化多次做 Render 一個 X 座標的程式碼,並且使之後增加功能更易於維護。 我發現對於牆壁後牆壁的 Render 可以設計為重複的工作,只是範圍不太一樣而已。 <center><img src="https://i.imgur.com/SqJr16G.png" style="width: 500px"></center> `raycaster_float.cpp` ```c=5 uint16_t Renderer::RecursiveTraceFrame( uint32_t *fb, // In, Frame buffer int x, // In, screen X uint16_t up, // In, upper screen position uint16_t down, // In, lower screen position uint8_t offset) // In, downscale { uint8_t sso; // top point of wall uint8_t tc; // x axis of texture (256 -> 64) uint8_t tn; // texture number uint16_t tso; // y axis of texture uint16_t tst; // texture step (currentlt not used) uint32_t *lb = fb + x + up * SCREEN_WIDTH; // frame buffer _rc->Trace(x, &sso, &tn, &tc, &tso, &tst); auto tx = static_cast<int>(tc >> 2); if (sso >= HORIZON_HEIGHT) sso = HORIZON_HEIGHT; // render top sky for (int y = up; y < HORIZON_HEIGHT - sso; y++) { if (offset > 0) *lb = DOWN_SCALE(*lb) + DOWN_SCALE(GetARGB(96 + (HORIZON_HEIGHT - y))); else *lb = GetARGB(96 + (HORIZON_HEIGHT - y)); lb += SCREEN_WIDTH; } // render obstacle for (int y = 0; y < sso * 2; y++) { // paint texture pixel uint16_t ty = (tso + (((TEXTURE_SIZE / 2) << 10) - tso) * y / sso) >> 10; uint16_t tv; uint32_t color; if (tn / 2 < NUMBER_OF_GRAY_TEXTURE) { // gray scale texture tv = (g_texture_gray[tn / 2])[(ty << 6) + tx]; color = GetARGB(tv); } else { // colored texture tv = (g_texture_color[tn / 2 - 1])[(ty << 6) + tx]; color = GetARGB_color(tv); } if (offset > 0) *lb = DOWN_SCALE(*lb) + DOWN_SCALE(color); else *lb = color; lb += SCREEN_WIDTH; } // render bottom sky for (int y = HORIZON_HEIGHT + sso; y < down; y++) { if (offset > 0) *lb = DOWN_SCALE(*lb) + DOWN_SCALE(GetARGB(96 + y - HORIZON_HEIGHT)); else *lb = GetARGB(96 + y - HORIZON_HEIGHT); lb += SCREEN_WIDTH; } return sso; } ``` 透明的方法就是保留原本一半的顏色,混入被遮蓋物體一半的顏色: $$ \left\{ \begin{array}{lr} B_{k+1} = \frac{1}{2}B_k + \frac{1}{2}C_k \\ B_0 = 0 \end{array} \right. $$ - $B_k$ 是第 k 次的 frame buffer - $C_k$ 是第 k 次的 trace 的顏色 為了簡化顏色 down scale 的計算,用這個 macro 將顏色變為原本的一半。 `renderer.h` ```c=7 #define DOWN_SCALE(color) (((color) & 0xFEFEFE) >> 1) ``` 剩下要注意的是: - first trace() 的時候,`frame buffer` 會殘留有過去的影像,因此直接覆值。 - second trace() (或是還有更多次的話),要更新為過去的一半 加上 現在的一半。 這樣一來,原本的 `TraceFrame()` 就可以寫得相當簡單: ```c=73 void Renderer::TraceFrame(Game *g, uint32_t *fb) { _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)); for (int x = 0; x < SCREEN_WIDTH; x++) { uint16_t sso = RecursiveTraceFrame(fb, x, 0, SCREEN_HEIGHT, 0); sso = RecursiveTraceFrame(fb, x, HORIZON_HEIGHT - sso, HORIZON_HEIGHT + sso, 1); } } ``` 由於視角始終都在地平線上的關係,可以保證下一次 trace 後的 sso 絕對比前一次的值要小 (距離遠的物體看起來比較矮) ||first trace|second trace| |:-:|:-:|:-:| |up|0|HORIZON_HEIGHT - sso| |down|SCREEN_HEIGHT|HORIZON_HEIGHT + sso| ### God mode 最後我新增一個 god mode 的模式,當按下 `g` 鍵後就會開啟透視外掛 `renderer.cpp` ```c=81 if (g->godMode > 0) sso = RecursiveTraceFrame(fb, x, HORIZON_HEIGHT - sso, HORIZON_HEIGHT + sso, 1); ``` `game.h` ```c=11 class Game { public: ... int godMode; ... }; ``` 效果: <center><img src="https://i.imgur.com/PexcrJA.png" style="width:500px"></center> #### 破壞場景 ## 增強遊戲體驗 #### 關於 Z 軸的可能性 好像,沒有這個可能 - ray 會穿透牆壁背後,也就是 `traceFrame()` 裡面的 `sso` 範圍會有好幾個,也很難判斷什麼時候要停。 - 頂部的 texture 要包含 X 方向 offset - 從最近開始 (螢幕下方) 直到撞到邊屆為止

    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