В предыдущих постах (раз, два, три) у нас шарики сталкивались друг с другом и отражались от границ, но теперь я решил добавить дополнительные препятствия в виде линий, от которых шарики будут отскакивать так же, как от границ - по "оптическому" закону. И если с вертикальными и горизонтальными границами все довольно просто - проверяешь не вышел ли обьект за границу x либо y компоненты и меняешь соответствующую компоненту скорости на противоположную (т.е. умножаешь на -1). А например в случае отражения шариков друг от друга мы просто сверяем расстояния между центрами шариков S с суммой их радиусов R1 + R2 и если S <= R1 + R2 то уже идет расчет новых векторов движения. Но в случае столкновения с протяженным обьектом все будет несколько иначе. Сначала надо представить центр шарика и два конца отрезка в виде треугольника:
p1 и p2 - это точки отрезка, точка A - это центр шарика. Имея все три точки, мы можем вычислить все углы треугольника и также его высоту h, используя вот такие формулы:
h = sin(p1) * (p1A)
Последняя формула - вычисление высоты (противолежащего катета) через синус угла и гипотенузу. Хотя у меня в моем маленьком векторном "фреймворке" есть операция определения угла между двумя векторами, но по причинам, в которых мне лень сейчас разбираться, она работает только для углов +-0.5 * Пи, а вот по формулам, приведенным выше, она работает и для тупых углов тоже (это важно). Но так же важно знать, что это довольно много вычислений и что нет смысла для каждого шарика (которых тысячи) каждый раз делать все эти вычисления сначала длины сторон, потом углов, потом высоты треугольника - это дорого. Сначала мы определим, что находимся ли мы где-то в рядом с отрезком и если да, то вот тогда уже вычисляем расстояние до него. Причем важно учесть, что в случае тупого угла у основания треугольника, надо брать не расстояние h, а расстояние p2A (расстояние от центра шарика до ближайшего конца отрезка). В общем, код выглядит так:
void checkCollision(const BorderLine& bl)
{
// rectangle around line
Point2f xyMin = bl.getXYMin();
xyMin = {xyMin.x - r, xyMin.y - r};
Point2f xyMax = bl.getXYMax();
xyMax = { xyMax.x + r, xyMax.y + r };
if (pos.x >= xyMin.x && pos.y >= xyMin.y && pos.x <= xyMax.x && pos.y <= xyMax.y)
{
Point2f a = pos;
// sides of triangle
float p1a = bl.p1.distanceTo(a);
float p2a = bl.p2.distanceTo(a);
float p1p2 = bl.p1.distanceTo(bl.p2);
// angles
float ang1 = acos((p1a * p1a + p1p2 * p1p2 - p2a * p2a) / (2 * p1a * p1p2));
float ang2 = acos((p2a * p2a + p1p2 * p1p2 - p1a * p1a) / (2 * p2a * p1p2));
// if both angles are sharp then calculate h (distanse from a to line)
if (ang1 <= k_PI / 2 && ang2 <= k_PI / 2)
{
float h = sin(ang1) * p1a;
if (h <= r)
{
// calculate contact point
float cath1 = cos(ang1) * p1a;
Point2f v = (bl.p2 - bl.p1).unitVector() * cath1;
Point2f contactPoint = bl.p1 + v;
opticCollPoint(contactPoint);
}
}
else if (p1a <= r)
{
opticCollPoint(bl.p1); // first line end
}
else if (p2a <= r)
{
opticCollPoint(bl.p2); // second line end
}
}
}
В строках 4-8 мы задаем прямоугольник, который охватывает нашу линию и проверяем, входит ли шарик в границы этого четырехугольника. Если да, то вычисляем стороны треугольника (строки 12-14) и углы у его основания (строки 16-17), причем основанием у нас является наш отрезок. Если оба угла острые, то вычисляем высоту треугольника (расстояние от центра шарика до отрезка) в строке 21. Если высота равна или меньше радиуса шарика - у нас коллизия! И тогда мы вычисляем точку контакта путем прибавления к одному из концов отрезка единичного вектора, умноженного на значение прилежащего катета (строки 25-27) и обрабатываем столкновение. Если же один из углов тупой, то смотрим, соприкасается ли шарик с одним из концов отрезка и если да, то обрабатываем столкновение (строки 31-37). Вот код этой функции:
void opticCollPoint(const Point2f contactPoint)
{
// direction to other contact point
Point2f toContPoint = contactPoint - pos;
// calculate dot product
float dotProd = f.dotProduct(toContPoint);
// if dot product is negative then force directed away
// from contact point and we do nothing
if (dotProd > 0)
{
// angle between normal and force (moving) vectors
float angle = f.angleTo(toContPoint);
// the angle of incidence is equal to the angle of reflection
f = Mat2x2f().rot(angle * 2) * f;
f = f * (-1.f);
}
}
Столкновение обрабатывается по "оптическому" принципу "угол падения равен углу отражения". На самом деле именно в эту оптическую формулу и вырождается тот же испульсный способ, если один из обьектов неповижен и имеет бесконечную (или на порядки большую) по сравнению со вторым обьектом массу.
Аналогичніе функции в для OpenCL:
Ball opticCollPoint(Ball b, float2 contactPoint)
{
float2 pos = (float2)(b.pos.x, b.pos.y);
// direction to other contact point
float2 toContPoint = contactPoint - pos;
// calculate dot product
float2 f = (float2)(b.f.x, b.f.y);
float dotProd = dot(f, toContPoint);
// if dot product is negative then force directed away
// from contact point and we do nothing
if (dotProd > 0)
{
// angle between normal and force (moving) vectors
float angle = getAngleTo(f, toContPoint);
// the angle of incidence is equal to the angle of reflection
f = rotVect(f, angle * 2);
f = f * (-1);
b.f.x = f.x;
b.f.y = f.y;
}
return b;
}
Ball checkCollisionBL(Ball b, BorderLine bl)
{
// rectangle around line
Point2f xyMin = getBLXYMin(bl);
xyMin.x = xyMin.x - b.r;
xyMin.y = xyMin.y - b.r;
Point2f xyMax = getBLXYMax(bl);
xyMax.x = xyMax.x + b.r;
xyMax.y = xyMax.y + b.r;
if (b.pos.x >= xyMin.x && b.pos.y >= xyMin.y && b.pos.x <= xyMax.x && b.pos.y <= xyMax.y)
{
float2 a = (float2)(b.pos.x, b.pos.y);
// sides of triangle
float2 blp1 = (float2)(bl.p1.x, bl.p1.y);
float2 blp2 = (float2)(bl.p2.x, bl.p2.y);
float p1a = getDistanceBetween(blp1, a);
float p2a = getDistanceBetween(blp2, a);
float p1p2 = getDistanceBetween(blp1, blp2);
// angles
float ang1 = acos((p1a * p1a + p1p2 * p1p2 - p2a * p2a) / (2 * p1a * p1p2));
float ang2 = acos((p2a * p2a + p1p2 * p1p2 - p1a * p1a) / (2 * p2a * p1p2));
// if both angles are sharp then calculate h (distanse from a to line)
if (ang1 <= M_PI_2_F && ang2 <= M_PI_2_F)
{
float h = sin(ang1) * p1a;
if (h <= b.r)
{
// calculate contact point
float cath1 = cos(ang1) * p1a;
float2 v = (blp2 - blp1);
v = getUnitVector(v) * cath1;
float2 contactPoint = blp1 + v;
b = opticCollPoint(b, contactPoint);
}
}
else if (p1a <= b.r) // first line end
{
b = opticCollPoint(b, blp1);
}
else if (p2a <= b.r) // second line end
{
b = opticCollPoint(b, blp2);
}
}
return b;
}
Пример работы:
Весь код находится
здесь.