力積・反発・摩擦
한국어 원문으로 표시합니다.
목표
충격량으로 충돌 반응을 계산하고, 반발 계수와 마찰이 무엇을 정하는지 숫자와 그림으로 확인합니다. 이 실습이 끝나면 물리 엔진의 반응 층을 직접 만들 수 있게 됩니다.
왜 중요한가
단단한 물체의 접촉 시간은 1밀리초도 되지 않아서, 60분의 1초로 도는 시뮬레이션에서는 접촉 과정을 흉내 낼 수 없습니다. 그래서 물리 엔진은 과정을 건너뛰고 결과만 계산합니다. 충돌 전후의 속도 관계를 방정식으로 세우고, 그것을 만족하는 속도 변화량을 한 번에 구해 적용합니다.
여기서 배워 둘 관례가 둘 있습니다. 하나는 역질량을 저장하는 것입니다. 움직이지 않는 물체를 역질량 0 으로 두면 특수 처리 없이 같은 식이 그대로 돕니다. 다른 하나는 멀어지는 중이면 손대지 않는 것입니다. 이 검사를 빠뜨리면 떨어지려는 물체를 다시 잡아당겨 표면에 달라붙습니다.
단계
/root/impulse에 도구 상자를 놓습니다./root/impulse/impulse.py— 충격량 공식./root/impulse/out/03-conserve.txt— 운동량과 에너지./root/impulse/out/bounce.csv,04-bounce.txt,bounce.png— 튀는 높이./root/impulse/out/05-friction.txt— 미끄러지는 상자./root/impulse/out/06-tangent.txt— 접선 방향 마찰./root/impulse/out/balls.png와07-balls.txt— 상자 안의 공 다섯 개.
참고
- 그림은
nohup python3 -m http.server 8080 -d /root/impulse/out &로 띄우고 웹 미리보기 에서http://localhost:8080/을 엽니다. - 흔한 실수 하나: 상대 속도가 이미 양수(멀어지는 중)인데도 충격량을 적용하는 것입니다. 물체가 표면에 달라붙습니다.
- 흔한 실수 둘: 마찰에서 속도를 0 에서 자르지 않는 것입니다. 속도가 음수로 내려가 상자가 뒤로 갑니다.
- 흔한 실수 셋: 접선 방향을 법선 충격량을 적용한 뒤의 속도로 구하는 것입니다. 값이 달라집니다.
그리기 도구 상자를 놓는다
/root/impulse/gfxlib.py 를 예시 그대로 저장하고, /root/impulse/check.py 로 시험 무늬를 그려 /root/impulse/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/impulse/impulse.py 에 resolve(v1, inv_m1, v2, inv_m2, n, e) 를 만드십시오. 속도는 2차원 튜플, n 은 1에서 2를 향하는 단위 벡터입니다. 상대 속도의 법선 성분이 0 이상이면 아무것도 하지 않고 그대로 돌려주고, 음수면 j = -(1+e)*v_rel/(inv_m1+inv_m2) 를 구해 v1 -= j*inv_m1*n, v2 += j*inv_m2*n 한 결과를 (v1, v2) 로 돌려줍니다.
v_rel = dot(v2 - v1, n) 입니다. 이 값이 양수라는 것은 두 물체가 이미 멀어지고 있다는 뜻이므로 손대면 안 됩니다. 이 검사를 빠뜨리면 떨어지려는 물체를 다시 잡아당겨 표면에 달라붙게 만듭니다.
질량이 아니라 역질량을 받는 이유는, 움직이지 않는 물체를 inv_m = 0 으로 자연스럽게 표현할 수 있기 때문입니다. 무한대를 넣으면 나눗셈마다 특수 처리가 필요합니다.
확인은 같은 질량 두 개를 정면충돌시켜 보는 것입니다. e=1 이면 속도가 서로 맞바뀌고, e=0 이면 둘 다 같은 속도가 됩니다.
무엇이 보존되고 무엇이 사라지나
질량 2 인 물체 1(속도 (3,0))과 질량 1 인 물체 2(속도 (-1,0))가 법선 (1,0) 으로 부딪히는 경우를 e=1, 0.5, 0 에 대해 풀어 /root/impulse/out/03-conserve.txt 에 p_before=, ke_before=, p_e1=, ke_e1=, p_e05=, ke_e05=, p_e0=, ke_e0= 여덟 줄을 적으십시오. 운동량은 x 성분의 합, 운동에너지는 0.5*m*v² 의 합입니다(소수점 아래 여섯 자리).
역질량은 각각 0.5 와 1.0 입니다.
운동량은 세 경우 모두 같아야 합니다. 두 물체가 크기가 같고 방향이 반대인 충격량을 받기 때문에 구조적으로 그렇습니다. 값이 다르게 나오면 부호가 틀린 것입니다.
운동에너지는 e=1 일 때만 보존되고, e 가 작아질수록 줄어듭니다. 사라진 에너지는 현실에서 소리와 열과 변형으로 갑니다.
e=0 이면 두 물체의 속도가 같아집니다. 붙어서 함께 움직인다는 뜻입니다.
튀는 높이는 e 제곱으로 줄어든다
공을 y=5.0, v=0 에서 떨어뜨려 g=-9.8, dt=1/240 로 2400걸음 굴리십시오. 한 걸음은 v += g*dt, y += v*dt 이고, y < 0 이 되면 y = -y, v = -0.7*v 로 되튕깁니다. 결과를 /root/impulse/out/bounce.csv 에 step,y,v 머리글과 2401줄로 적고, 처음 세 번의 되튄 최고 높이를 /root/impulse/out/04-bounce.txt 에 peak1=, peak2=, peak3= 으로 적으십시오. 그리고 /root/impulse/out/bounce.png(256x256, 검은 배경)에 px = int(step*255/2400), py = 255 - int(y/5.0*255) 로 색 (120,200,255) 점을 찍으십시오.
되튄 최고 높이는 y 가 이웃한 두 값보다 크거나 같은 지점(국소 최댓값)입니다. 처음 세 개를 고르면 됩니다.
비율을 계산해 보십시오. peak2/peak1 과 peak3/peak2 가 모두 e² = 0.49 근처로 나와야 합니다. 속도가 e 배로 줄면 올라가는 높이는 e 제곱 배로 줄기 때문입니다.
그래프를 보면 봉우리가 점점 낮아지는 익숙한 모양이 나옵니다. 이 그림이 반발 계수의 의미를 한 장으로 보여 줍니다.
y = -y 로 되튕기는 것은 파고든 만큼을 반사시켜 되돌려 주는 방식입니다. 0 으로 잘라 버리는 것보다 에너지 손실이 적습니다.
미끄러지는 상자는 어디서 멈추나
속도 5.0 으로 미끄러지는 상자에 쿨롱 마찰(mu=0.3, g=9.8)을 걸어 dt=1/240 로 굴리십시오. 한 걸음은 v = max(0, v - mu*g*dt), x += v*dt 이고, v 가 0 이 되면 멈춘 것입니다. 걸린 시간과 이동 거리를 /root/impulse/out/05-friction.txt 에 steps=, stop_time=, distance= 세 줄로 적으십시오.
마찰은 속도에 비례하지 않고 일정한 감속을 줍니다. 그래서 속도가 직선으로 줄어들고, 시간과 거리를 해석적으로 구할 수 있습니다.
t = v0 / (mu * g) = 5 / 2.94 = 1.7007 s
d = v0² / (2 * mu * g) = 25 / 5.88 = 4.2517 m
직접 굴린 값이 이 식과 1퍼센트 안쪽으로 맞아야 합니다. 정확히 같지는 않은데, 이산적인 걸음이 마지막 조각을 잘라 내기 때문입니다.
max(0, ...) 이 없으면 속도가 음수로 내려가 상자가 뒤로 갑니다. 마찰이 물체를 밀어 주는 셈이 되는데, 이것은 실제 엔진에서도 흔한 실수입니다.
마찰도 충격량이다
/root/impulse/tangent.py 에 bounce_with_friction(v, inv_m, n, e, mu) 를 만드십시오. 움직이지 않는 표면(역질량 0)에 부딪히는 물체 하나를 다룹니다. 법선 충격량 jn = -(1+e)*dot(v,n)/inv_m 을 구해 적용한 뒤, 접선 방향 t = normalize(v - dot(v,n)*n) 에 대해 jt = -dot(v,t)/inv_m 을 구하고 [-mu*jn, +mu*jn] 으로 자른 값을 적용해 최종 속도를 돌려줍니다. (v_out, jn, jt) 세 값을 돌려주고, v=(3,-3), inv_m=1, n=(0,1), e=0.5, mu=0.4 인 경우를 /root/impulse/out/06-tangent.txt 에 vout=vx,vy, jn=, jt= 로 적으십시오.
접선 방향은 속도에서 법선 성분을 뺀 나머지입니다. 그 나머지가 0 이면(정면으로 부딪힌 경우) 접선이 정의되지 않으므로 마찰을 건너뛰어야 합니다.
jt 를 자르는 것이 쿨롱 마찰의 전부입니다. 접선 속도를 완전히 없애는 데 필요한 값이 mu*jn 을 넘지 않으면 물체가 그 자리에 멈추고(정지 마찰), 넘으면 잘린 값만 적용되어 미끄러집니다(운동 마찰). 조건문 하나로 두 마찰이 다 나옵니다.
주어진 경우에서는 jn=4.5, 자르기 전 jt=-3, 상한이 0.4*4.5=1.8 이라 jt=-1.8 이 됩니다. 최종 속도는 (1.2, 1.5) 입니다.
법선 성분과 접선 성분은 jn 을 적용하기 전의 속도로 계산합니다. 순서를 섞으면 값이 달라집니다.
상자 안의 공 다섯 개
반지름 0.5 인 공 다섯 개를 한 변 10 인 상자 안에서 g=-9.8, dt=1/240, 벽 반발 계수 0.9 로 1800걸음 굴리십시오. 처음 상태는 순서대로 위치 (2,8)·(5,9)·(8,7)·(3,5)·(7,4), 속도 (3,0)·(-2,1)·(0,0)·(4,2)·(-3,-1) 입니다. 10걸음마다 각 공의 위치를 /root/impulse/out/balls.png(256x256, 검은 배경, sx=int(x*25.6), sy=int(255-y*25.6))에 색 (255,100,100)·(100,255,100)·(100,150,255)·(255,220,80)·(220,120,255) 로 찍고, /root/impulse/out/07-balls.txt 에 b1=x,y,vx,vy 부터 b5= 까지와 energy_start=, energy_end= 를 적으십시오. 에너지는 sum(0.5*(vx²+vy²) + 9.8*y) 입니다.
벽 처리는 축마다 따로 합니다. x - r < 0 이면 x = r, vx = -e*vx 이고 반대쪽도 같습니다. y 도 같습니다.
한 걸음의 순서는 중력 적용, 위치 갱신, 벽 처리입니다. 순서를 바꾸면 공이 벽을 뚫습니다.
에너지는 반드시 줄어들어야 합니다. 반발 계수가 1 보다 작으니 벽에 부딪힐 때마다 조금씩 잃습니다. 늘어났다면 벽 처리에서 위치를 되돌리는 부분이 빠졌거나 부호가 틀린 것입니다.
궤적이 그림에 남습니다. 포물선 조각들이 이어져 튀는 모습이 눈에 보입니다.