Отражение от стены: формула зеркала
Любой отскок — от стены, ракетки или наклонной плиты — это одна и та же формула отражения через нормаль.
Отражение вектора от поверхности — преобразование, при котором составляющая скорости вдоль нормали меняет знак, а вдоль поверхности сохраняется.
Формула отражения
Пусть мяч летит со скоростью $\vec v$ и ударяется о стену с единичной нормалью $\hat n$ (перпендикуляр к поверхности). Отражённая скорость $\vec v\,'$ вычисляется через скалярное произведение:
$$\vec v\,' = \vec v - 2(\vec v \cdot \hat n)\,\hat n.$$
Смысл прозрачен: $(\vec v\cdot\hat n)\,\hat n$ — это составляющая скорости вдоль нормали (та, что «вбивается» в стену). Мы вычитаем её дважды: первый раз убираем, второй — добавляем в обратную сторону. В итоге нормальная компонента меняет знак (мяч отскакивает), а касательная (вдоль стены) остаётся прежней (мяч скользит вдоль). Это закон «угол падения равен углу отражения», записанный одной строкой и работающий для стены под любым наклоном.
Проверим на простых стенах
def dot(a, b): return a[0]*b[0] + a[1]*b[1]
def reflect(v, n): # n — единичная нормаль
d = dot(v, n)
return (v[0] - 2*d*n[0], v[1] - 2*d*n[1])
print("от вертикальной стены, нормаль (1,0):")
print(" (2,-3) ->", reflect((2, -3), (1, 0)))
print("от пола, нормаль (0,1):")
print(" (2,-3) ->", reflect((2, -3), (0, 1)))
Вывод:
от вертикальной стены, нормаль (1,0): (2,-3) -> (-2, -3) от пола, нормаль (0,1): (2,-3) -> (2, 3)
От вертикальной стены инвертируется горизонтальная компонента ($v_x$ из $2$ стал $-2$), вертикальная не тронута. От пола наоборот: $v_y$ из $-3$ стал $+3$. Формула сама «понимает», какую компоненту разворачивать, потому что нормаль указывает направление удара.
Мяч в коробке
Соберём классическую сцену: мяч летает внутри квадрата $10\times10$ и отскакивает от стенок. Для осевых стен отражение сводится к смене знака соответствующей скорости.
x, y = 2.0, 3.0
vx, vy = 1.5, 1.0
dt = 1.0
for t in range(1, 9):
x += vx*dt; y += vy*dt
hit = ""
if x < 0 or x > 10:
vx = -vx; x = max(0, min(10, x)); hit += "x"
if y < 0 or y > 10:
vy = -vy; y = max(0, min(10, y)); hit += "y"
print(f"t={t} pos=({x:5.2f},{y:5.2f}) vel=({vx:+.1f},{vy:+.1f}) {hit}")
Вывод:
t=1 pos=( 3.50, 4.00) vel=(+1.5,+1.0) t=2 pos=( 5.00, 5.00) vel=(+1.5,+1.0) t=3 pos=( 6.50, 6.00) vel=(+1.5,+1.0) t=4 pos=( 8.00, 7.00) vel=(+1.5,+1.0) t=5 pos=( 9.50, 8.00) vel=(+1.5,+1.0) t=6 pos=(10.00, 9.00) vel=(-1.5,+1.0) x t=7 pos=( 8.50,10.00) vel=(-1.5,+1.0) t=8 pos=( 7.00,10.00) vel=(-1.5,-1.0) y
На шаге $6$ мяч достиг правой стены ($x = 10$), и $v_x$ сменил знак — мяч пошёл влево. На шаге $8$ он коснулся потолка, и развернулась уже вертикальная скорость. Это и есть отражение в действии — сердце аркад вроде Pong и Breakout.
Как работает под капотом
Для осевых стен мы могли просто инвертировать одну компоненту, но общая формула через нормаль работает для любой ориентации стены — наклонной плиты, борта пинбола, грани полигона. Достаточно знать единичную нормаль поверхности. Заметьте важную деталь: после разворота скорости мы ещё и «возвращаем» мяч внутрь (max/min), потому что за шаг он мог чуть выйти за стену. Без этой коррекции мяч мог бы «прилипнуть» к стене и дребезжать, разворачиваясь каждый кадр. Аккуратная обработка проникновения — типичная забота коллизий, к которой мы вернёмся в разделе про столкновения.
Частые ошибки
- Нормаль не единичная. Формула $\vec v - 2(\vec v\cdot\hat n)\hat n$ предполагает $|\hat n| = 1$; иначе масштаб отражения исказится.
- Забыть вернуть тело из стены. Без коррекции позиции мяч «застревает» и дребезжит у стены.
- Разворачивать обе компоненты сразу. При ударе о вертикальную стену меняется только $v_x$; разворот обеих — это отскок «назад», а не отражение.
Итог
- Отражение: $\vec v\,' = \vec v - 2(\vec v\cdot\hat n)\hat n$, нормаль $\hat n$ единичная.
- Нормальная компонента инвертируется, касательная сохраняется (угол падения = угол отражения).
- Формула работает для стены любой ориентации, не только осевой.
- После отскока нужно вернуть тело внутрь, иначе оно «прилипнет» к стене.