1.4 SDF的碰撞檢測與碰撞響應
前面提到φ(x)≤0表示點x在障礙物內,那么碰撞檢測只需要得到點的φ值,然后與碰撞半徑r比較即可,φ(x)≤r表示角色與障礙物發生了碰撞。由于柵格地圖的SDF數據是離散存儲的,但角色移動是連續的,不能把角色在一個柵格內任意位置的φ值等同于柵格頂點,否則會在柵格邊界產生巨變。因此,在移動是連續的情況下,無法直接查表獲取角色所在位置的φ值,如圖1.4所示圓的圓心位置,需要根據周邊柵格頂點的φ值采樣獲取。

圖1.4 如何計算圓心的φ值
因為距離本身是線性的,可以采用雙線性過濾(Bilinear Filtering)采樣角色位置的φ值,根據角色所處柵格的四個頂點線性插值可得到場景任意點的φ值,如圖1.5所示。

圖1.5 雙線性過濾示意圖

由此完成SDF的碰撞檢測,只需要查表和乘法計算,時間復雜度為O(1)。以下為插值獲得場景任意點的SD值的代碼。
// 計算位置pos的SD值 // 每個柵格的實際尺寸為grid,橫向柵格數量為width public float Sample(Vector2 pos) { pos = pos / grid; int fx = Mathf.FloorToInt(pos.x); int fy = Mathf.FloorToInt(pos.y); float rx = pos.x - fx; float ry = pos.y - fy; int i = fy * width + fx; return (sdf[i ] * (1- rx) + sdf[i + 1] * rx) * (1- ry) + (sdf[i + width] * (1- rx) + sdf[i + width + 1] * rx) * ry; }
當前幾乎所有的MOBA手游在搖桿移動過程中,碰到障礙物之后均是繞著障礙物滑行的,而不是直接停止,因為停止的體驗實在很糟糕。那么SDF在發生碰撞后如何處理繞障礙物滑行的問題呢?
如圖1.6所示,v表示搖桿方向(角色原始移動方向),當與障礙物發生碰撞后需要沿著v′方向滑行,v′和v的關系是


圖1.6 滑行
上式中,n為碰撞法線,如何獲取呢?
SDF為純量場,純量場中某一點上的梯度(Gradient)指向純量場增長最快的方向,因此可以利用SDF的梯度作為碰撞法線:

同時,φ(x)幾乎隨處可導,可以使用有限差分法(Finite Difference)求出x處的梯度:

從而得到碰撞法線n,求出在滑行方向實現碰撞后繞障礙物滑行。以下為求梯度方向的代碼。
// 求位置pos的梯度方向 public Vector2 Gradient(Vector2 pos) { float delta = 1f; return 0.5f * new Vector2( Sample(new Vector2(pos.x + delta, pos.y)) - Sample(new Vector2(pos.x - delta, pos.y)), Sample(new Vector2(pos.x, pos.y + delta)) - Sample(new Vector2(pos.x, pos.y - delta))); }
至此,得到當角色按搖桿方向移動時的實際移動方向代碼。
// 獲取在移動過程中使用SDF得到的最佳位置 public Vector2 GetVaildPositionBySDF(Vector2 pos, Vector2 dir, float speed) { Vector2 newPos = pos + dir * speed; float SD = Sample(newPos); // 不可行走 if (SD < playerRadius) { Vector2 gradient = Gradient(newPos); Vector2 adjustDir = dir - gradient * Vector2.Dot(gradient, dir); newPos = pos + adjustDir.normalized * speed; // 多次迭代 for (int i = 0; i < 3; i++) { SD = Sample(newPos); if (SD >= playerRadius) break; newPos += Gradient(newPos) * (playerRadius - SD); } // 避免往返 if (Vector2.Dot(newPos - pos, dir) < 0) newPos = pos; } return newPos; }