LabHub
배우기 러닝패스 코스

The Skeleton of a Physics Engine

Stack Five Boxes and Settle Them

LabHub 에서 이어서 보기

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

목표

파고든 겹침을 위치 보정으로 되돌리고, 반복 횟수로 더미의 단단함을 조절하고, 상자 다섯 개가 쌓여 안정되는 과정을 그림으로 남깁니다. 이 실습이 코스 2의 마무리입니다.

왜 중요한가

충격량은 속도를 고칠 뿐 이미 파고든 겹침을 되돌리지 못합니다. 겹침을 알아채는 시점이 이미 겹친 뒤이기 때문입니다. 그래서 위치를 따로 밀어 주어야 하는데, 여기에 두 개의 손잡이가 있습니다. 한 걸음에 몇 퍼센트를 되돌릴지(percent)와 무시할 여유(slop)입니다. 한 번에 다 밀면 튕기고, 여유를 두지 않으면 미세하게 떨립니다. 겹침을 정확히 0 으로 만들려 하지 않는 것이 오히려 안정적이라는 점이 이 층의 핵심입니다.

그리고 접촉이 여럿이면 서로 얽힙니다. 위를 고치면 아래가 눌리고 아래를 고치면 위가 뜹니다. 정확히 풀려면 연립방정식이 필요한데 비싸므로, 엔진들은 같은 순회를 여러 번 반복해 근사합니다. 반복 횟수가 곧 더미가 얼마나 단단해 보이는지를 정합니다.

단계

  1. /root/stack 에 도구 상자를 놓습니다.
  2. /root/stack/correct.py — 위치 보정.
  3. /root/stack/out/03-sink.txt — 보정 없이 가라앉는 깊이.
  4. /root/stack/out/04-fixed.txt — 보정을 켠 깊이.
  5. /root/stack/out/05-iters.txt — 반복 횟수 1·4·10.
  6. /root/stack/out/06-stack.txtstack.png — 다섯 개를 쌓기.
  7. /root/stack/out/f00.png ~ f11.pngindex.html — 쌓이는 과정.

참고

그리기 도구 상자를 놓는다

/root/stack/gfxlib.py 를 예시 그대로 저장하고, /root/stack/check.py 로 시험 무늬를 그려 /root/stack/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/stack/correct.pycorrect(depth, slop, percent, inv_m1, inv_m2) 를 만드십시오. 보정량 = max(depth - slop, 0) / (inv_m1 + inv_m2) * percent 를 구해 (보정량*inv_m1, 보정량*inv_m2) 두 실수를 돌려줍니다. 물체 1 은 그만큼 -n 방향으로, 물체 2 는 +n 방향으로 옮긴다는 뜻입니다. 역질량의 합이 0 이면 (0.0, 0.0) 을 돌려주십시오.

max(depth - slop, 0) 이 슬롭의 전부입니다. 겹침이 여유보다 얕으면 아무것도 하지 않습니다. 이게 없으면 겹침을 정확히 0 으로 만들려는 보정이 매 걸음 아주 작은 값으로 계속 일어나 물체가 미세하게 떨립니다.

percent 는 한 걸음에 몇 퍼센트를 되돌릴지입니다. 1.0 이면 한 번에 다 밀어내는데, 그러면 물체가 튕겨 나가고 다음 걸음에 다시 부딪히는 진동이 생깁니다. 보통 0.2 에서 0.8 을 씁니다.

역질량으로 나눈 뒤 다시 각자의 역질량을 곱하는 이유는, 가벼운 쪽이 더 많이 움직이게 하기 위해서입니다. 한쪽이 움직이지 않는 물체(역질량 0)면 반대쪽이 전부 움직입니다.

보정이 없으면 가라앉은 채로 굳는다

반높이 0.5 인 상자 하나를 y=1.5, v=0 에서 떨어뜨려 g=-9.8, dt=1/60 로 600걸음 굴리십시오. 한 걸음은 (1) v += g*dt, (2) 반복 10회로 0.5 - y > 0 이고 v < 0 이면 v = 0, (3) y += v*dt 이고 위치 보정은 하지 않습니다. 마지막 관통 깊이 0.5 - y/root/stack/out/03-sink.txtdepth_final= 로 적으십시오(음수면 0).

속도를 0 으로 만들어도 이미 파고든 만큼은 그대로 남습니다. 겹침을 알아채는 시점이 이미 겹친 뒤이기 때문입니다.

그리고 그 상태에서 다음 걸음의 중력이 다시 속도를 주고, 위치가 조금 더 내려가고, 다시 0 이 됩니다. 깊이가 어떤 값에서 멈추는데 그것이 이 단계의 답입니다.

600걸음 뒤 깊이는 0.02 보다 큽니다. 상자 높이의 몇 퍼센트에 해당하는 값인데, 상자를 여러 개 쌓으면 이 오차가 쌓여 더미가 눈에 띄게 낮아집니다.

v = 0 으로 두는 것은 반발 계수 0 인 충격량과 같습니다. 바닥의 역질량이 0 이라 상자 쪽만 바뀝니다.

보정을 켜면 슬롭까지 돌아온다

같은 조건에 (4) 위치 보정 단계를 더하십시오 — 위치 갱신 뒤에 depth = 0.5 - y 를 구해 depth > slop 이면 y += percent*(depth - slop) 입니다(slop=0.01, percent=0.2). 600걸음 뒤의 깊이를 /root/stack/out/04-fixed.txtdepth_final= 로, 3단계 값과의 차이를 improved= 로 적으십시오.

보정은 위치만 옮깁니다. 속도를 함께 바꾸면 보정이 물체를 밀어 주는 힘이 되어 더미가 스스로 튀어 오릅니다.

깊이가 슬롭 근처에서 멈춥니다. 0 이 되지 않는 것이 정상입니다 — 아주 얇게 겹친 상태로 두는 편이 오히려 안정적이기 때문입니다.

improved3단계 깊이 - 4단계 깊이 입니다. 양수여야 하고, 대략 3단계 깊이의 절반을 넘습니다.

바닥은 움직이지 않으므로 상자 쪽만 옮깁니다. 상자끼리라면 역질량 비율로 나눠 양쪽을 밀어야 합니다.

반복 횟수가 더미의 단단함을 정한다

반높이 0.5 인 상자 세 개를 y = 0.5 + i + 1.0(i=0,1,2)에서 떨어뜨려 600걸음 굴리되 반복 횟수만 1, 4, 10 으로 바꿔 각각의 최대 관통 깊이를 /root/stack/out/05-iters.txtiters1=, iters4=, iters10= 로 적으십시오. 위치 보정은 켭니다(slop=0.01, percent=0.2).

접촉이 셋입니다 — 바닥-0번, 0번-1번, 1번-2번. 반복 안에서 이 순서대로 풉니다.

상자끼리의 접촉은 이렇습니다. 겹침 1.0 - (y[i+1] - y[i]) 이 0보다 크고 상대 속도 v[i+1] - v[i] 가 음수이면, j = -(v[i+1]-v[i]) / (inv_i + inv_{i+1}) 를 구해 v[i] -= j*inv_i, v[i+1] += j*inv_{i+1} 입니다. 모든 상자의 역질량은 1 입니다.

위치 보정도 세 접촉에 모두 겁니다. 상자끼리는 m = percent*(depth-slop)/(inv_i+inv_{i+1}) 을 구해 아래쪽을 m*inv_i 만큼 내리고 위쪽을 m*inv_{i+1} 만큼 올립니다.

최대 관통 깊이는 세 접촉의 깊이 중 가장 큰 값입니다. 반복을 늘릴수록 작아져야 합니다 — 그게 이 단계에서 보려는 것입니다.

다섯 개를 쌓는다

상자 다섯 개를 y = 0.5 + 2.0 + i*1.6(i=0..4)에서 떨어뜨려 반복 15회로 900걸음 굴리고, 마지막 높이 다섯 개와 최대 관통 깊이를 /root/stack/out/06-stack.txty1= ~ y5=, max_depth= 로 적으십시오. 그리고 마지막 상태를 /root/stack/out/stack.png(256x256, 배경 (20,20,28))에 그리십시오 — 상자는 가로 반폭 1.5, 화면 좌표는 sx = int(x*25.6 + 128), sy = int(255 - y*25.6) 이고 색은 아래부터 (230,120,110)·(230,190,110)·(150,220,120)·(120,190,230)·(190,140,230) 이고, 마지막에 sy=255 줄을 흰색으로 그어 바닥을 표시합니다.

처음 높이를 어긋나게 두면 상자들이 차례로 내려앉아 쌓이는 모습이 나옵니다. 같은 간격으로 두면 한꺼번에 떨어져 볼 것이 없습니다.

상자 하나는 화면에서 가로 89부터 166, 세로는 int(255-(y+0.5)*25.6) 부터 int(255-(y-0.5)*25.6) 까지의 사각형입니다. Canvasfill_rect 를 쓰면 한 줄입니다.

맨 아래 상자는 슬롭만큼 파고들어 있어서 사각형이 sy=255 까지 내려옵니다. 그래서 바닥 줄을 먼저 그으면 상자가 그것을 덮어 버립니다. 바닥은 마지막에 그으십시오.

마지막 높이가 대략 0.47, 1.44, 2.41, 3.38, 4.37 근처로 나옵니다. 정확히 0.5, 1.5, 2.5 가 아닌 이유는 슬롭만큼 겹친 채로 안정되기 때문입니다.

최대 관통 깊이는 0.05 를 넘지 않아야 합니다. 넘는다면 반복 횟수가 모자라거나 위치 보정이 빠진 것입니다.

쌓이는 과정을 열두 장으로

같은 시뮬레이션에서 0걸음째부터 15걸음마다 상태를 찍어 /root/stack/out/f00.png 부터 f11.png 까지 열두 장을 만들고, 열두 이름을 모두 담은 /root/stack/out/index.html 을 만들어 python3 -m http.server 8080 -d /root/stack/out 으로 확인하십시오. 그림 규칙은 6단계와 같습니다.

0, 15, 30, ... 165 걸음째의 상태를 저장했다가 나중에 한꺼번에 그리면 됩니다. 시뮬레이션을 열두 번 다시 돌릴 필요가 없습니다.

마지막 두 장이 거의 같아야 정상입니다. 더미가 안정되었다는 것을 숫자로 확인하는 방법이 바로 그것입니다 — 프레임 사이의 차이가 0 으로 수렴하는가.

반대로 첫 장과 마지막 장은 확연히 달라야 합니다. 첫 장은 상자들이 공중에 흩어져 있고 마지막 장은 바닥에 쌓여 있습니다.

HTML 은 앞 실습과 같은 방식으로 <img> 하나와 setInterval 이면 됩니다.