LabHub
배우기 러닝패스 코스

物理エンジンの骨組み

重なりを見つける四つの方法

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

AABB, 원, 분리축 정리로 겹침을 판정하고, 넓은 단계로 검사 횟수를 줄이고, 광선과 상자의 교차를 풉니다. 이 실습이 끝나면 물리 엔진의 충돌 감지 층을 직접 만들 수 있게 됩니다.

왜 중요한가

물체가 200개면 쌍이 19,900개입니다. 전부 정밀 판정을 하면 물리 계산이 시작되기도 전에 프레임이 끝납니다. 그런데 실제로 겹치는 쌍은 300개도 되지 않습니다. 그래서 충돌 감지는 두 층으로 나뉩니다 — 겹칠 수도 없는 쌍을 아주 싸게 걸러 내는 넓은 단계와, 남은 쌍을 정확히 보는 좁은 단계입니다.

좁은 단계의 일반해가 분리축 정리입니다. 두 볼록 도형이 떨어져 있다면 그것을 갈라놓는 직선이 반드시 존재하고, 그 방향은 두 도형의 변 중 하나와 나란합니다. 그래서 변 법선 방향으로만 투영해 보면 됩니다. 그리고 겹쳤을 때 가장 작은 겹침을 준 축이 곧 가장 짧게 밀어낼 방향이라, 감지와 반응이 같은 계산에서 나옵니다.

단계

  1. /root/collide 에 도구 상자를 놓습니다.
  2. /root/collide/aabb.py — 축 정렬 경계 상자.
  3. /root/collide/circle.py — 원과 원.
  4. /root/collide/sat.py — 분리축 정리.
  5. /root/collide/out/sat.png — 겹친 도형과 최소 이동 벡터.
  6. /root/collide/boxes.txt/root/collide/out/06-broad.txt — 전수 검사 대 정렬-훑기.
  7. /root/collide/ray.py/root/collide/out/07-ray.txt, ray.png — 광선과 상자.

참고

그리기 도구 상자를 놓는다

/root/collide/gfxlib.py 를 예시 그대로 저장하고, /root/collide/check.py 로 시험 무늬를 그려 /root/collide/out/00-check.png 를 만드십시오. 무늬는 64x64 검은 배경에 (0,0)에서 (63,63)까지 흰색(255,255,255) 대각선을 긋고, 그 뒤에 (0,32)에서 (63,32)까지 빨강(255,0,0) 가로선을 겹쳐 그은 것입니다.

이 실습부터는 PNG 인코더를 다시 만들지 않습니다. 첫 실습에서 손으로 만든 것과 같은 코드를 도구로 내려 줍니다 — 여기서 배울 것은 파일 형식이 아니기 때문입니다.

실습 파드에는 볼륨이 없어서 앞 실습에서 만든 파일이 남아 있지 않습니다. 그래서 실습마다 도구 상자를 다시 놓는 것으로 시작합니다.

Canvas(w, h, bg) 를 만들고 line(x0, y0, x1, y1, rgb) 로 두 선을 그은 뒤 write_png(path) 로 저장하십시오. 가로선을 나중에 그어야 교차점 (32,32) 가 빨강이 됩니다.

이 실습에서는 겹친 도형과 최소 이동 벡터를 그리는 데 씁니다.

축 정렬 경계 상자

/root/collide/aabb.pyoverlap(a, b)penetration(a, b) 를 만드십시오. 상자는 (minx, miny, maxx, maxy) 네 실수이고, 경계가 정확히 맞닿은 경우도 겹침으로 봅니다. penetration 은 겹치면 축별 겹침 폭 (dx, dy) 를, 겹치지 않으면 (0.0, 0.0) 을 돌려줍니다.

겹치지 않는 조건을 먼저 쓰는 편이 훨씬 짧습니다.

not (a[2] < b[0] or b[2] < a[0] or a[3] < b[1] or b[3] < a[1])

부등호에 등호를 넣을지 말지가 곧 '맞닿은 경우' 의 판정입니다. 이 실습은 맞닿음을 겹침으로 보므로 위 식처럼 등호를 뺍니다.

겹침 폭은 min(a[2], b[2]) - max(a[0], b[0]) 입니다. 두 상자의 오른쪽 끝 중 왼쪽에 있는 것에서, 왼쪽 끝 중 오른쪽에 있는 것을 뺀 값입니다.

판정 규칙은 한 곳에만 두십시오. 여러 곳에서 다르게 정하면 상자가 바닥에서 떨렸다 붙었다 합니다.

원과 원

/root/collide/circle.pycircle_hit(c1, r1, c2, r2) 를 만드십시오. 중심 사이 거리가 반지름의 합보다 작을 때만 겹친 것으로 보고 (True, 법선, 깊이) 를, 아니면 (False, (0.0, 0.0), 0.0) 을 돌려줍니다. 법선은 1에서 2를 향하는 단위 벡터이고 깊이는 r1 + r2 - 거리 입니다. 두 중심이 정확히 같으면 법선을 (1.0, 0.0) 으로 둡니다.

거리가 0 일 때를 따로 처리하지 않으면 0으로 나누기가 납니다. 물체 두 개가 정확히 같은 자리에서 태어나는 일은 실제로 생각보다 자주 있습니다 — 스포너에서 한꺼번에 만들 때가 그렇습니다.

비교만 할 것이라면 제곱근을 피할 수 있지만, 여기서는 깊이를 구해야 하므로 실제 거리가 필요합니다.

법선의 방향을 1에서 2로 정해 두는 것이 중요합니다. 다음 모듈의 충격량 계산이 이 약속을 그대로 씁니다. 반대로 두면 물체가 서로를 끌어당깁니다.

분리축 정리

/root/collide/sat.pysat_hit(a, b) 를 만드십시오. a, b 는 반시계 방향으로 적은 볼록 다각형(꼭짓점 목록)이고, 겹치면 (True, 축, 깊이) 를, 아니면 (False, (0.0,0.0), 0.0) 을 돌려줍니다. 축은 두 도형의 모든 변 법선 중 겹침이 가장 작은 것이고, a 의 무게중심에서 b 의 무게중심을 향하도록 부호를 맞춥니다.

(x0,y0) -> (x1,y1) 의 법선은 (ey, -ex) 를 정규화한 것입니다(ex = x1-x0, ey = y1-y0).

한 축에서의 겹침은 min(a1, b1) - max(a0, b0) 이고, 이 값이 0 이하이면 그 축이 두 도형을 갈라놓는 축이므로 즉시 끝내면 됩니다. 나머지 축을 볼 필요가 없습니다.

마지막에 축의 부호를 맞추는 이유는, 변 법선이 어느 도형의 것이냐에 따라 방향이 반대로 나올 수 있기 때문입니다. dot(축, 무게중심b - 무게중심a) < 0 이면 뒤집으십시오.

정사각형 두 개가 x 방향으로 1.5 만큼 떨어져 있으면 축은 (1,0), 깊이는 0.5 가 나와야 합니다.

겹침과 밀어낼 방향을 그린다

/root/collide/draw.py/root/collide/out/sat.png(256x256, 검은 배경)를 만드십시오. 다각형 A 는 [(-1,-1),(1,-1),(1,1),(-1,1)] 을 흰색(255,255,255) 테두리로, B 는 A 를 (1.5, 0.5) 만큼 옮긴 것을 노랑(255,220,60) 테두리로 그리고, B 의 무게중심에서 축 * 깊이 만큼 뻗는 최소 이동 벡터를 빨강(255,60,60) 선으로 그립니다. 화면 좌표는 sx = 128 + 40*x, sy = 128 - 40*y 입니다.

다각형 테두리는 꼭짓점을 순서대로 잇고 마지막에서 첫 점으로 돌아오는 선분들입니다. gfxlib.Canvasline 을 쓰십시오.

최소 이동 벡터는 sat_hit(A, B) 가 돌려준 축과 깊이를 곱한 것입니다. 이 만큼 B 를 밀면 두 도형이 정확히 떨어집니다 — 그것이 '최소' 라는 말의 뜻입니다.

그리는 순서는 A, B, 화살표입니다. 겹치는 자리에서는 나중에 그린 것이 이깁니다.

그림을 보면 두 사각형이 x 방향으로만 살짝 겹쳐 있고 화살표가 오른쪽을 가리키는 것이 보입니다. y 방향 겹침이 더 깊은데도 x 를 고르는 이유가 '가장 짧게 밀어내는 방향' 이기 때문입니다.

정렬해 놓고 훑는다

/root/collide/gen_boxes.py 를 예시 그대로 저장해 실행하면 /root/collide/boxes.txt 에 상자 200개가 만들어집니다. 그 상자들에 대해 전수 검사와 정렬-훑기를 각각 돌려 /root/collide/out/06-broad.txtpairs_brute=, pairs_sweep=, checks_brute=, checks_sweep= 네 줄을 적으십시오.

전수 검사는 i < j 인 모든 쌍을 보는 것이고 검사 횟수는 200*199/2 = 19,900 입니다.

정렬-훑기는 이렇습니다. 상자를 minx 로 정렬해 두면, 어떤 상자와 겹칠 수 있는 것은 정렬 순서에서 뒤에 이어지는 것 중 minx 가 이 상자의 maxx 를 넘지 않는 것들뿐입니다. 그 지점에서 안쪽 반복문을 break 하십시오.

checks_sweep 은 안쪽 반복문에서 실제로 겹침 판정을 부른 횟수입니다(break 로 빠져나간 것은 세지 않습니다).

두 방법이 찾은 쌍의 개수는 반드시 같아야 합니다. 넓은 단계는 답을 바꾸는 것이 아니라 같은 답을 싸게 얻는 것입니다. 다르다면 break 조건이 틀린 것입니다.

광선과 상자 — 슬랩 방법

/root/collide/ray.pyray_aabb(o, d, box) 를 만들어 광선 다섯 개를 상자 (-1,-1,1,1) 에 쏘고, 결과를 /root/collide/out/07-ray.txtr1=hit,t 또는 r1=miss,0.000000 형식으로 다섯 줄 적으십시오. 그리고 /root/collide/out/ray.png(256x256)에 상자를 흰색으로, 맞은 광선은 초록(60,255,120) 으로 맞은 지점까지, 빗나간 광선은 빨강(255,80,80) 으로 길이 6 만큼 그리십시오. 화면 좌표는 sx = 128 + 30*x, sy = 128 - 30*y 입니다. 광선은 순서대로 o=(-3,0) d=(1,0), o=(-3,3) d=(1,0), o=(0,0) d=(1,0), o=(-3,-3) d=(0.7071067811865476, 0.7071067811865476), o=(3,0) d=(1,0) 입니다.

슬랩 방법은 축마다 광선이 그 축의 두 평면 사이에 있는 구간 [t1, t2] 를 구하고, 모든 축의 구간을 교집합으로 좁혀 나가는 것입니다.

t1 = (box.min - o) / d      # 방향이 음수면 t1 과 t2 가 뒤바뀐다
t2 = (box.max - o) / d
tmin = max(tmin, min(t1, t2))
tmax = min(tmax, max(t1, t2))

방향 성분이 0 이면 나눗셈이 터집니다. 그때는 원점이 그 축의 구간 안에 있는지만 보고, 밖이면 빗나간 것입니다.

마지막 판정은 tmax >= max(tmin, 0) 입니다. 0 을 넣는 이유는 광선이 뒤쪽으로 뻗지 않기 때문입니다. 이걸 빠뜨리면 광선 뒤에 있는 상자도 맞았다고 나옵니다. 다섯 번째 광선이 정확히 그 경우입니다.

광선의 출발점이 상자 안에 있으면 t 가 0 입니다(세 번째 광선).