# 2020q3 Homework5 (render)
contributed by < `ccs100203` >
###### tags: `linux2020`
> [I07: render](https://hackmd.io/@sysprog/2020-render)
## Raycasting
raycasting 的原理是在 screen 範圍內打出一條條的光線,偵測物體距離,藉此把 2D 轉為 3D
![](https://upload.wikimedia.org/wikipedia/commons/e/e7/Simple_raycasting_with_fisheye_correction.gif)
在專案內 `renderer.cpp` 中的 `TraceFrame` 實現此原理
用 `x` 去模擬每一條光線
```cpp=11
for (int x = 0; x < SCREEN_WIDTH; x++)
```
:::warning
嘗試將原理補充的更詳細
:::
## Fixing Bugs
### 1. 接近牆面時 floating-point 運算缺失 (Overflow)
![](https://i.imgur.com/lIw1V2c.png)
floating-point 的計算主要由 `raycaster_float.cpp` 負責,`void RayCasterFloat::Trace` 即是負責計算每條光線 (每個 column)。由於這是在貼近牆面時出現的 bug,研判跟 `distance` (與牆面的距離) 有關聯,所以在程式中找尋使用到 `distance` 的地方。
接著在程式中發現以下問題,151 行的除法,很可能在 `distance` 是一個小於 1 的值出問題。觀察三個變數的 type,會發現當 `INV_FACTOR / distance` 大於 255 時就會發生 overflow,導致 render 錯誤。
```cpp=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;
}
```
```cpp
uint8_t *screenY;
#define INV_FACTOR (float) (SCREEN_WIDTH * 95.0f / 320.0f)
float distance = lineDistance * cos(deltaAngle);
```
我不懂這裡的運作原理,只好純粹對 overflow 的問題做修正。
- 為了讓 `txs` 得到正確的值,我設立了一個 `tmp` 作為 `INV_FACTOR / distance` 的答案。這樣已經有效修正 `txs` 產生錯誤的情況
- 再來的問題是 `screenY` 應該設為多少
觀察 renderer.cpp 內的程式,其中 `sso` 即為 `screenY`,發現當 `sso` 大於 `HORIZON_HEIGHT` 時會被修正為 `HORIZON_HEIGHT`。而在 `Trace` 內會發生此情況,代表 `txs > SCREEN_HEIGHT` 成立,因為 `txs` 原先應為 `*screenY * 2.0f`,而 `HORIZON_HEIGHT` 又等同 `SCREEN_HEIGHT / 2`。所以在此條件成立時將 `screenY` 設為 `HORIZON_HEIGHT;`
```cpp=22
int16_t ws = HORIZON_HEIGHT - sso;
if (ws < 0) {
ws = 0;
sso = HORIZON_HEIGHT;
}
```
以下是改良後的版本
```cpp=150
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;
}
```
- 改良後的結果
![](https://i.imgur.com/Qq6AWWx.png)
### 2. fixed-point 在角落往內看會出現奇怪牆面 (`else` Condition Missing)
因為跑到邊邊才能看到這個 bug,所以推斷是在 `distance` 處在邊界時才會發生
![](https://i.imgur.com/88iKEkE.png)
由 raycaster_fixed.cpp 處理 fixed-point 的計算,一樣在程式內尋找使用 `distance` 的地方。
- 在 `LookupHeight` 內有處理 `distance` 在邊界時的情況
發現在 76 行有判斷式不周全的情況,即便 `if (ds >= 256)` 成立也會在接下來的運算把正確的值覆蓋掉,所以把缺少的 else 補上
```cpp=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);
}
}
```
改良後的程式
```cpp=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);
}else {
*height = LOOKUP8(g_farHeight, ds);
*step = LOOKUP16(g_farStep, ds);
}
} else {
*height = LOOKUP8(g_nearHeight, distance);
*step = LOOKUP16(g_nearStep, distance);
}
}
```
- 改良後的結果
![](https://i.imgur.com/mQQqiI4.png)
### 3. floating-point 在邊界往外看牆面會消失 (`delta` is wrong)
![](https://i.imgur.com/Xvw1Swm.png)
後來經過測試發現是在 `playerX` 或 `playerY` 有趨近 1.0 的情況時就會發生牆面消失,反之則不會
![](https://i.imgur.com/LQv9ZqW.png)
研判是在計算 distance 時出現問題,為了了解 distance 計算方式,從 [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 內部的運算就好。
這邊是全部修改完的程式
```cpp=51
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));
}
```
- 修改後的結果
![](https://i.imgur.com/tV0ahZK.png)
### 4. 在邊界的牆面比例不同 (wall position is wrong)
![](https://i.imgur.com/YSkBzCJ.png)
經過實驗後發現只有在 `playerX` 或 `playerY` 為最大值 (約 30) 時會發生這種情形,也就是圖上的紅色牆面,而在綠色牆面是正常的。
![](https://i.imgur.com/VWSqoW8.png)
藉由這張圖發現 fixed-point 判斷邊界牆面的位置時出現問題,明顯可以看出左邊的牆面 (邊界),應往內 (右) 一格。
![](https://i.imgur.com/XfJuh9C.png)
已經知道問題是判斷牆面的位置出錯,所以去找程式內的 `IsWall`,來比較一下 fixed-point 與 floating-point 的 `IsWall`
- raycaster_fixed.cpp
```cpp=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
```cpp=6
bool RayCasterFloat::IsWall(float rayX, float rayY)
{
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);
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)));
}
```
因為已經知道會在 X 或 Y 在最大值時出現問題,於是推斷在與 `MAP_X` 或 `MAP_Y` 比較時有錯誤,因為這兩個變數代表著地圖的右邊界與上邊界。
繼續觀察可以發現兩份程式的判斷式不同,一個是 `>` 一個是 `>=`,顯然這就是造成牆面位置相差 1 的原因。
```cpp=
// fixed
if (tileX > MAP_X - 1 || tileY > MAP_Y - 1)
// float
if (tileX < 0 || tileY < 0 || tileX >= MAP_X - 1 || tileY >= MAP_Y - 1)
```
因為現在出問題的是 fixed-point 的牆面,所以把他的判斷式修正成 `>=`,下面是修正後的程式
```cpp=61
inline bool RayCasterFixed::IsWall(uint8_t tileX, uint8_t tileY)
{
if (tileX >= MAP_X - 1 || tileY >= MAP_Y - 1) {
return true;
}
return LOOKUP8(g_map, (tileX >> 3) + (tileY << (MAP_XS - 3))) &
(1 << (8 - (tileX & 0x7)));
}
```
- 修正後的結果
![](https://i.imgur.com/mkCfH99.png)
#### 解釋為什麼認為出問題的是 fixed-point 而不是 floating-point
觀察以下的圖片
![](https://i.imgur.com/WhIGZhT.png)
![](https://i.imgur.com/mUgzjbe.png)
第一張是綠色的牆面,也就是正常的牆面
第二張是紅色的牆面,也就是異常的牆面
可以看出牆面在畫面中的比例,floating-point 是維持一致的,但是 fixed-point 的牆面比例明顯改變,故我認為判斷條件寫錯的是 fixed-point。
## 在編譯時期產生 Table
TODO