---
# System prepended metadata

title: 2020q3 Homework5 (render)
tags: [sysprog2020]

---

# 2020q3 Homework5 (render)
contribute by <`simpson0114`>
###### tags: `sysprog2020`

----

## 作業要求
- [ ] 修正浮點數和定點數算繪程式展現的缺失，並提出改進 precision 及 accuracy 的方式
- [ ] 輸出算繪過程的 frame rate，日後當我們進一步提升算繪程式的效率時，這會是效能評比的方式之一
- [ ] 利用 tools/precalculator.cpp 產生運算表格，修改相關程式碼，使得程式碼在編譯時期才去產生運算表格，後者以標頭檔案 (generated header) 的形式存在並編譯進入主程式
- [ ] 解說現有 fixed-point 實作機制，並探討前述表格產生的機制，需要提及其中的考量點
- [ ] 將 raycaster 以 C99/C11 (或 gnu99/gnu11) 重寫，允許在執行時期載入 fixed-point 和 floating-point 為基礎的 renderer


## 程式執行
進入資料夾 make 後，輸入 `./main` 即可執行程式


## 程式原理

### Raycasting
raycasting 的原理是在 screen 範圍內打出一條條的光線，偵測物體距離，藉此把 2D 轉為 3D

![reference link](https://upload.wikimedia.org/wikipedia/commons/e/e7/Simple_raycasting_with_fisheye_correction.gif)

在專案內 renderer.cpp 中的 TraceFrame 實現此原理
用 x 去模擬每一條光線

```c=11
for (int x = 0; x < SCREEN_WIDTH; x++)
```

## 修正程式缺失
### floating-point 靠近牆壁畫面破碎
![](https://i.imgur.com/1iDzn2M.png)
原程式碼 `raycaster_float.cpp`：150
```c=150
if (distance > 0) {
    *screenY = INV_FACTOR / distance;
    auto txs = (*screenY * 2.0f);
    if (txs != 0) {
        *textureStep = (256 / txs) * 256;
        if (txs > SCREEN_HEIGHT) {
            auto wallHeight = (txs - SCREEN_HEIGHT) / 2;
            *textureY = wallHeight * (256 / txs) * 256;
        }
    }
} else {
    *screenY = 0;
}
```
可觀察到因為 `*screenY` type 為 `uint8_t` ，所以 `*screenY` 會 overflow ，因此我先修正了 overflow 的問題。

又由 `renderer.cpp`：22
```c=22
nt16_t ws = HORIZON_HEIGHT - sso;
if (ws < 0) {
    ws = 0;
    sso = HORIZON_HEIGHT;
}
```

因在 `renderer.cpp` 中 `sso` 就是 `*screenY` ，可觀察到若 `sso` > `HORIZON_HIGHT` ，則 `sso` = `HORIZON_HIGHT` ，因此若 `*screenY` overflow ，則將 `*screenY` 設為 `HORIZON_HIGHT`

以下為修改後程式碼
`raycaster_float.cpp`：136
```c=136
if (distance > 0) {
    float tmp = INV_FACTOR / distance;
    *screenY = tmp;
    auto txs = (tmp * 2.0f);
    if (txs != 0) {
        *textureStep = (256 / txs) * 256;
        if (txs > SCREEN_HEIGHT) {
            auto wallHeight = (txs - SCREEN_HEIGHT) / 2;
            *textureY = wallHeight * (256 / txs) * 256;
            *screenY = HORIZON_HEIGHT;
        }
    }
} else {
    *screenY = 0;
}
```

修正後：
![reference link](https://i.imgur.com/H6AltO5.png)

### floating-point 角落牆面消失
![reference link](https://i.imgur.com/Wu2Gmla.png)

因為在角落才會出現這個 bug ，所以推斷是 `distance` 在邊界時才會發生。

測試後發現是在 `playerX` 或 `playerX` 有趨近 1.0 的情況時就會發生牆面消失，反之則不會。

參考影片 [Wolfenstein 3D's map renderer](https://www.youtube.com/watch?v=eOCQfxRQ2pY) 去釐清原理。

在 raycaster_float.cpp 中測試，經過實驗發現在 `offsetX` 或 `offsetY` 等於 0 時會發生此情形。影片中 $xIntercept = x + dx + -dy / \tan(\theta)$ 對應到程式內的 `float interceptX = rayX + startDeltaX;`。而 `offset` 是影響到 `startDelta` 的關鍵，所以在決定 `startDeltaX`  和 `startDeltaY` 的值時出問題。

這邊看到程式在 offset 為 0 時會特別處理
```cpp=56
if (offsetY == 0) {
    startDeltaX = (1) * fabs(tan(rayA));
} else {
    startDeltaX = (offsetY) *fabs(tan(rayA));
}
```

我認為這樣很奇怪，按照式子來看 $-dy / \tan(\theta)$ 應該為 0，這邊卻是 $(1) * fabs(\tan(\theta))$。所以對式子進行修正，將 1 改為 0

```cpp=56
if (offsetY == 0) {
    startDeltaX = (0) * fabs(tan(rayA));
} else {
    startDeltaX = (offsetY) *fabs(tan(rayA));
}
```

發現原本的問題解決了，於是接著精簡程式碼。當 if 成立時 offset 必定是 0，所以整個判斷式變得多餘，直接保留 else 內部的運算就好。


以下為修正的程式碼
```c=52
float startDeltaX, startDeltaY;
if (rayA <= M_PI_2) {
    startDeltaX = (1 - offsetY) * tan(rayA);
    startDeltaY = (1 - offsetX) / tan(rayA);
} else if (rayA <= M_PI) {
    startDeltaX = (offsetY) *fabs(tan(rayA));
    startDeltaY = -(1 - offsetX) / fabs(tan(rayA));
} else if (rayA < 3 * M_PI_2) {
    startDeltaX = -(offsetY) *fabs(tan(rayA));
    startDeltaY = -(offsetX) / fabs(tan(rayA));

} else {
    startDeltaX = -(1 - offsetY) * fabs(tan(rayA));
    startDeltaY = (offsetX) / fabs(tan(rayA));
}
```

修正後：
![reference link](https://i.imgur.com/h47KnXS.png)

### fixed-point 角落多一面牆面
![reference link](https://i.imgur.com/RBowOh3.png)

因為也是在角落才會出現的問題，所以判斷是 `distance` 在邊界才會發生。 

觀察 `raycaster_fixed.cpp` 處理 fixed-point 的計算，一樣在程式內尋找使用 distance 的地方。

`raycaster_fixed.cpp`：74
```c=74
if (distance >= 256) {
    const uint16_t ds = distance >> 3;
    if (ds >= 256) {
        *height = LOOKUP8(g_farHeight, 255) - 1;
        *step = LOOKUP16(g_farStep, 255);
    }
    *height = LOOKUP8(g_farHeight, ds);
    *step = LOOKUP16(g_farStep, ds);
} else {
    *height = LOOKUP8(g_nearHeight, distance);
    *step = LOOKUP16(g_nearStep, distance);
}
```

可觀察到在 76 行，當 `ds` 超過 256 時會將 `*height` 和 `*step` 修正，但隨後會再把修正覆蓋掉。

修正後程式：
`raycaster_fixed.cpp`：74
```c=74
if (distance >= 256) {
    const uint16_t ds = distance >> 3;
    if (ds >= 256) {
        *height = LOOKUP8(g_farHeight, 255) - 1;
        *step = LOOKUP16(g_farStep, 255);
    }
    else {
        *height = LOOKUP8(g_farHeight, ds);
        *step = LOOKUP16(g_farStep, ds);
    }
} else {
    *height = LOOKUP8(g_nearHeight, distance);
    *step = LOOKUP16(g_nearStep, distance);
}
```

修正後：
![reference link](https://i.imgur.com/gCTQsXt.png)

### fixed-point 牆面距離不正確

可觀察下面兩張圖，可發現 fixed-point 在不同牆面距離會有所差距，但 floating-point 不會有差距，因此可以推測是 fixed-point 有問題。

![reference link](https://i.imgur.com/16nxIaF.png)

![reference link](https://i.imgur.com/3aBi75I.png)

觀察 `raycaster_float.cpp` 和 `raycaster_fixed.cpp` 中 `IsWall` 的差別。

可觀察到
`raycaster_fixed.cpp`
```c=61
inline bool RayCasterFixed::IsWall(uint8_t tileX, uint8_t tileY)
{
    if (tileX > MAP_X - 1 || tileY > MAP_Y - 1) {
        return true;
    }
    return LOOKUP8(g_map, (tileX >> 3) + (tileY << (MAP_XS - 3))) &
           (1 << (8 - (tileX & 0x7)));
}
```


`raycaster_float.cpp`
```c=16
if (tileX < 0 || tileY < 0 || tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) {
    return true;
}
return g_map[(tileX >> 3) + (tileY << (MAP_XS - 3))] &
       (1 << (8 - (tileX & 0x7)));
```

可觀察到 `raycaster_fixed.cpp` 中的第 63 行的 if 判斷式並沒有判斷到等於的情況

因此修正後程式碼如下：
`raycaster_fixed.cpp`
```c=61
inline bool RayCasterFixed::IsWall(uint8_t tileX, uint8_t tileY)
{
    if (tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) {
        return true;
    }
    return LOOKUP8(g_map, (tileX >> 3) + (tileY << (MAP_XS - 3))) &
           (1 << (8 - (tileX & 0x7)));
}
```

修正後：
![reference link](https://i.imgur.com/69kS3cP.png)

### 發現 `g_map` 中地圖與實際不同
![reference link](https://i.imgur.com/MgH9vrY.png)

觀察 `raycaster_float.cpp` 及 `raycaster_fixed.cpp` 可得知 `IsWall()` 是判斷障礙物與否，而障礙物資訊是紀錄在 `raycaster-data.h` 的 `g_map` 中


由 `IsWall()` 推斷 `g_map` 有關資訊如下：
* A bit as a pixel.
* 大小為正方形 (32x32) pixel, 四個 uint8_t element 為一個 X axis ，共分成 32 rows, that is, Y axis.

按照二維方向排列應該長這樣：
```
0b00000000, 0b00000000, 0b00000000, 0b00000000, 
0b00000000, 0b00000000, 0b00000000, 0b00000000, 
...
0b00000000, 0b00000000, 0b00000000, 0b00000000, 
```
橫向 32 bit，縱向 32 rows.

**圖片大小推測**
`raycaster_float.cpp` line:16
```c=16
if (tileX < 0 || tileY < 0 || tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) {
    return true;
}
```
`MAP_X` $=$ `MAP_Y` $= 32$
由以上程式碼可推斷若 `tileX` 或 `tileY` $< 0$ 以及 `tileX` 或 `tileY` $>= 31$ ，則為超過邊界
因此可得知地圖大小為 $32*32$

`raycaster_float.cpp` line:19
```c=19
return g_map[(tileX >> 3) + (tileY << (MAP_XS - 3))] &
    (1 << (8 - (tileX & 0x7)));
```

* 此段程式碼為判斷座標 (`tileX`, `tileY`) 是否為障礙物，由於 `g_map` 中，每個 element 皆為 `uint_8` ，因此每個 element 可以代表 8 個 pixel，$8=2^3$，因此 `tileX` 需除 8 ，也就是 >> 3 。
* `g_map` 是 1d array ，因此要計算 Y 方向應除掉 X 方向的長度， X 方向共包含 32 個 bit ($32=2^5$) ，但是一個 element 可以代表 8 個 bit ，因此，應除 $32/8=4$ 或 `>> 4`。
* `MAP_XS` 的意義為地圖大小的 2 指數，即 `2^5=32` ，其中 32 為 `MAP_X` ，`MAP_XS` 為指數部分。
* 第 18 行用來取 X axis 的 bit ，需注意的是，地圖中 LSB 為 X 正向， MSB 為 X 反向，與數值大小方向相反。取 7 的原因為，一個 element 可以代表 8 個 bit，這 8 個 bit 恰好是 X 座標的 3 個 LSBs 因此取 3 個 LSB 全為 1 的情況，即為 `0x7`。
    :::warning
    這邊的 `8-(tileX & 0x7)` 應該錯了，考慮 `tileX` 最大即最小情況：
    - 最小 `tileX` =0 時，`8-0=8`：1<<8，會超過 uint8_t 的範圍
    - 最大 `tileX` =7 時，`8-7=1`：1<<1=2，發現永遠沒有 1 的情況
    
    因此應該將 8 修正為 7。
    :::
    
修正後可發現障礙物位置與 `g_map` 相同