четверг, 7 марта 2024 г.

2д коллайдер: добавление пользовательских препятствий / 2D collider: add custom obstacles

 В предыдущих постах (раз, два, три) у нас шарики сталкивались друг с другом и отражались от границ, но теперь я решил добавить дополнительные препятствия в виде линий, от которых шарики будут отскакивать так же, как от границ - по "оптическому" закону. И если с границами все довольно просто - проверяешь не вышел ли обьект за границу и меняешь соответствующую компоненту скорости на противоположную (т.е. умножаешь на -1), а например в случае отражения шариков друг от друга мы просто сверяем расстояния между центрами шариков S с суммой их радиусов R1 + R2 и если S <= R1 + R2 то уже идет расчет новых векторов движения, но в случае столкновения с протяженным обьектом все будет несколько иначе. Сначала надо представить центр шарика и два конца отрезка в виде треугольника:

p1 и p2 - это точки отрезка, точка A - это центр шарика. Имея все три точки, мы можем вычислить все углы треугольника и также его высоту h, используя вот такие формулы:
h = sin(p1) * (p1A)

Последняя формула - вычисление высоты (противолежащего катета) через синус угла и гипотенузу. Хотя у меня в моем маленьком векторном "фреймворке" есть операция определения угла между двумя векторами, но по причинам, в которых мне лень сейчас разбираться, она работает только для углов +-0.5 * Пи, а вот по формулам, приведенным выше,  она работает и для тупых углов тоже (это важно). Но так же важно знать, что это довольно много вычислений и что нет смысла для каждого шарика (которых тысячи) каждый раз делать все эти вычисления сначала длины сторон, потом углов, потом высоты треугольника - это дорого. Сначала мы определим, что находимся ли мы где-то в рядом с отрезком и если да, то вот тогда уже вычисляем расстояние до него. Причем важно учесть, что в случае тупого угла у основания треугольника, надо брать не расстояние h, а расстояние p2A. В общем, код выглядит так:

  1. void checkCollision(const BorderLine& bl)
  2. {
  3. // rectangle around line
  4. Point2f xyMin = bl.getXYMin();
  5. xyMin = {xyMin.x - r, xyMin.y - r};
  6. Point2f xyMax = bl.getXYMax();
  7. xyMax = { xyMax.x + r, xyMax.y + r };
  8. if (pos.x >= xyMin.x && pos.y >= xyMin.y && pos.x <= xyMax.x && pos.y <= xyMax.y)
  9. {
  10. Point2f a = pos;
  11. // sides of triangle
  12. float p1a = bl.p1.distanceTo(a);
  13. float p2a = bl.p2.distanceTo(a);
  14. float p1p2 = bl.p1.distanceTo(bl.p2);
  15. // angles
  16. float ang1 = acos((p1a * p1a + p1p2 * p1p2 - p2a * p2a) / (2 * p1a * p1p2));
  17. float ang2 = acos((p2a * p2a + p1p2 * p1p2 - p1a * p1a) / (2 * p2a * p1p2));
  18. // if both angles are sharp then calculate h (distanse from a to line)
  19. if (ang1 <= k_PI / 2 && ang2 <= k_PI / 2)
  20. {
  21. float h = sin(ang1) * p1a;
  22. if (h <= r)
  23. {
  24. // calculate contact point
  25. float cath1 = cos(ang1) * p1a;
  26. Point2f v = (bl.p2 - bl.p1).unitVector() * cath1;
  27. Point2f contactPoint = bl.p1 + v;
  28. opticCollPoint(contactPoint);
  29. }
  30. }
  31. else if (p1a <= r)
  32. {
  33. opticCollPoint(bl.p1); // first line end
  34. }
  35. else if (p2a <= r)
  36. {
  37. opticCollPoint(bl.p2); // second line end
  38. }
  39. }
  40. }
В строках 4-8 мы задаем прямоугольник, который охватывает нашу линию и проверяем, входит ли шарик в границы этого четырехугольника. Если да, то вычисляем стороны треугольника (строки 12-14) и углы у его основания (строки 16-17), причем основанием у нас является наш отрезок. Если оба угла острые, то вычисляем высоту треугольника (расстояние от центра шарика до отрезка) в строке 21. Если высота равна или меньше радиуса шарика - у нас коллизия! И тогда мы вычисляем точку контакта путем прибавления к одному из концов отрезка единичного вектора, умноженного на значение прилежащего катета (строки 25-27) и обрабатываем столкновение. Если же один из углов тупой, то смотрим, соприкасается ли шарик с одним из концов отрезка и если да, то обрабатываем столкновение (строки 31-37). Вот код этой функции:

  1. void opticCollPoint(const Point2f contactPoint)
  2. {
  3. // direction to other contact point
  4. Point2f toContPoint = contactPoint - pos;
  5. // calculate dot product
  6. float dotProd = f.dotProduct(toContPoint);
  7. // if dot product is negative then force directed away
  8. // from contact point and we do nothing
  9. if (dotProd > 0)
  10. {
  11. // angle between normal and force (moving) vectors
  12. float angle = f.angleTo(toContPoint);
  13. // the angle of incidence is equal to the angle of reflection
  14. f = Mat2x2f().rot(angle * 2) * f;
  15. f = f * (-1.f);
  16. }
  17. }
Столкновение обрабатывается по "оптическому" принципу "угол падения равен углу отражения". На самом деле именно в эту оптическую формулу и вырождается тот же испульсный способ, если один из обьектов неповижен и имеет бесконечную (или на порядки большую) по сравнению со вторым обьектом массу.
Аналогичніе функции в для OpenCL:

  1. Ball opticCollPoint(Ball b, float2 contactPoint)
  2. {
  3. float2 pos = (float2)(b.pos.x, b.pos.y);
  4. // direction to other contact point
  5. float2 toContPoint = contactPoint - pos;
  6. // calculate dot product
  7. float2 f = (float2)(b.f.x, b.f.y);
  8. float dotProd = dot(f, toContPoint);
  9. // if dot product is negative then force directed away
  10. // from contact point and we do nothing
  11. if (dotProd > 0)
  12. {
  13. // angle between normal and force (moving) vectors
  14. float angle = getAngleTo(f, toContPoint);
  15. // the angle of incidence is equal to the angle of reflection
  16. f = rotVect(f, angle * 2);
  17. f = f * (-1);
  18. b.f.x = f.x;
  19. b.f.y = f.y;
  20. }
  21. return b;
  22. }
  23.  
  24. Ball checkCollisionBL(Ball b, BorderLine bl)
  25. {
  26. // rectangle around line
  27. Point2f xyMin = getBLXYMin(bl);
  28. xyMin.x = xyMin.x - b.r;
  29. xyMin.y = xyMin.y - b.r;
  30. Point2f xyMax = getBLXYMax(bl);
  31. xyMax.x = xyMax.x + b.r;
  32. xyMax.y = xyMax.y + b.r;
  33. if (b.pos.x >= xyMin.x && b.pos.y >= xyMin.y && b.pos.x <= xyMax.x && b.pos.y <= xyMax.y)
  34. {
  35. float2 a = (float2)(b.pos.x, b.pos.y);
  36. // sides of triangle
  37. float2 blp1 = (float2)(bl.p1.x, bl.p1.y);
  38. float2 blp2 = (float2)(bl.p2.x, bl.p2.y);
  39. float p1a = getDistanceBetween(blp1, a);
  40. float p2a = getDistanceBetween(blp2, a);
  41. float p1p2 = getDistanceBetween(blp1, blp2);
  42. // angles
  43. float ang1 = acos((p1a * p1a + p1p2 * p1p2 - p2a * p2a) / (2 * p1a * p1p2));
  44. float ang2 = acos((p2a * p2a + p1p2 * p1p2 - p1a * p1a) / (2 * p2a * p1p2));
  45. // if both angles are sharp then calculate h (distanse from a to line)
  46. if (ang1 <= M_PI_2_F && ang2 <= M_PI_2_F)
  47. {
  48. float h = sin(ang1) * p1a;
  49. if (h <= b.r)
  50. {
  51. // calculate contact point
  52. float cath1 = cos(ang1) * p1a;
  53. float2 v = (blp2 - blp1);
  54. v = getUnitVector(v) * cath1;
  55. float2 contactPoint = blp1 + v;
  56. b = opticCollPoint(b, contactPoint);
  57. }
  58. }
  59. else if (p1a <= b.r) // first line end
  60. {
  61. b = opticCollPoint(b, blp1);
  62. }
  63. else if (p2a <= b.r) // second line end
  64. {
  65. b = opticCollPoint(b, blp2);
  66. }
  67. }
  68. return b;
  69. }
Пример работы:


Весь код находится здесь.