上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人
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; }