箱を五つ積んで安定させる
한국어 원문으로 표시합니다.
목표
파고든 겹침을 위치 보정으로 되돌리고, 반복 횟수로 더미의 단단함을 조절하고, 상자 다섯 개가 쌓여 안정되는 과정을 그림으로 남깁니다. 이 실습이 코스 2의 마무리입니다.
왜 중요한가
충격량은 속도를 고칠 뿐 이미 파고든 겹침을 되돌리지 못합니다. 겹침을 알아채는 시점이 이미 겹친 뒤이기 때문입니다. 그래서 위치를 따로 밀어 주어야 하는데, 여기에 두 개의 손잡이가 있습니다. 한 걸음에 몇 퍼센트를 되돌릴지(percent)와 무시할 여유(slop)입니다. 한 번에 다 밀면 튕기고, 여유를 두지 않으면 미세하게 떨립니다. 겹침을 정확히 0 으로 만들려 하지 않는 것이 오히려 안정적이라는 점이 이 층의 핵심입니다.
그리고 접촉이 여럿이면 서로 얽힙니다. 위를 고치면 아래가 눌리고 아래를 고치면 위가 뜹니다. 정확히 풀려면 연립방정식이 필요한데 비싸므로, 엔진들은 같은 순회를 여러 번 반복해 근사합니다. 반복 횟수가 곧 더미가 얼마나 단단해 보이는지를 정합니다.
단계
/root/stack에 도구 상자를 놓습니다./root/stack/correct.py— 위치 보정./root/stack/out/03-sink.txt— 보정 없이 가라앉는 깊이./root/stack/out/04-fixed.txt— 보정을 켠 깊이./root/stack/out/05-iters.txt— 반복 횟수 1·4·10./root/stack/out/06-stack.txt와stack.png— 다섯 개를 쌓기./root/stack/out/f00.png~f11.png와index.html— 쌓이는 과정.
참고
- 그림은
nohup python3 -m http.server 8080 -d /root/stack/out &로 띄우고 웹 미리보기 에서http://localhost:8080/을 엽니다. 열두 장이 순서대로 돌면 상자가 떨어져 쌓이는 것이 보입니다. - 한 걸음의 순서를 정확히 지키십시오 — 중력, 반복 속도 해법, 위치 갱신, 위치 보정입니다. 순서가 다르면 값이 달라집니다.
- 흔한 실수 하나: 위치 보정에서 속도까지 바꾸는 것입니다. 보정이 힘이 되어 더미가 스스로 튀어 오릅니다.
- 흔한 실수 둘: 슬롭 없이 겹침을 0 으로 만들려는 것입니다. 매 걸음 아주 작은 보정이 계속 일어나 상자가 떨립니다.
그리기 도구 상자를 놓는다
/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.py 에 correct(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.txt 에 depth_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.txt 에 depth_final= 로, 3단계 값과의 차이를 improved= 로 적으십시오.
보정은 위치만 옮깁니다. 속도를 함께 바꾸면 보정이 물체를 밀어 주는 힘이 되어 더미가 스스로 튀어 오릅니다.
깊이가 슬롭 근처에서 멈춥니다. 0 이 되지 않는 것이 정상입니다 — 아주 얇게 겹친 상태로 두는 편이 오히려 안정적이기 때문입니다.
improved 는 3단계 깊이 - 4단계 깊이 입니다. 양수여야 하고, 대략 3단계 깊이의 절반을 넘습니다.
바닥은 움직이지 않으므로 상자 쪽만 옮깁니다. 상자끼리라면 역질량 비율로 나눠 양쪽을 밀어야 합니다.
반복 횟수가 더미의 단단함을 정한다
반높이 0.5 인 상자 세 개를 y = 0.5 + i + 1.0(i=0,1,2)에서 떨어뜨려 600걸음 굴리되 반복 횟수만 1, 4, 10 으로 바꿔 각각의 최대 관통 깊이를 /root/stack/out/05-iters.txt 에 iters1=, 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.txt 에 y1= ~ 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) 까지의 사각형입니다. Canvas 의 fill_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 이면 됩니다.