파이썬 예제 (힘과 가속도)

Force and Acceleration


이전 벡터 마우스 트래킹예제는 나름 훌륭하지만 조금 아쉬운 부분이 있습니다.

바로 이동 물체에 대한 질량 개념이 없어 가속도 구현이 마치 자석에 이끌리듯 마우스를 쫓아 가는 모습이었습니다.


이제 뉴턴의 제2 운동법칙 F=MA을 적용해볼 시점이 된 것 같습니다.

F(Force)는 힘이고, M(Mass)은 질량, A(Acceleration)는 가속도 입니다.

따라서 힘은 질량과 가속도를 곱한 값입니다.

이 수식을 살짝 바꾸면 아래와 같습니다.

A = F / M (가속도는 힘을 질량으로 나눈 값이다)

즉, 있는 힘을 다해 밀더라도 물체가 무겁다면 가속도는 느려지겠지요. 살짝 밀더라도 물체가 가볍다면 빠르게 가속할 것입니다.

조금 더 이해를 쉽게 하기 위해 M(질량)이 1이라고 가정한다면 A = F 라는 결과를 얻고, 바로 가속도는 힘과 같습니다.

이 원리를 이용해 중력을 흉내내는 코드를 만들어 보았습니다.

[예제 실행화면]



먼저, 물체의 위치, 속도, 가속도 벡터를 묶어서 클래스로 만듭니다.

1. mover.py 파일 코드

from Physics.vector import vector
from PyQt5.QtGui import QColor
from random import randint

class Mover:
    def __init__(self, x=0.0, y=0.0, mass = 1.0):
        self.location = vector(x,y)
        self.velocity = vector()
        self.acceleration = vector() 

        # 질량
        self.mass = mass
        
        self.G = 1.0

        self.color = QColor(randint(0,255), randint(0,255), randint(0,255), 128)        

    def applyForce(self, force):        
        # 뉴턴 운동 2법칙 (F=MA or A = F/M)
        force/=vector(self.mass, self.mass)
        self.acceleration += force


즉 이 예제를 실행하기 위해서는 vector.py, mover.py, gravity.py 총 3개의 파일이 필요하지만 vector.py는 이전 게시물과 내용이 같으므로 생략하겠습니다.

첫 라인의 Physics.vector는 이전 예제(벡터, 마우스 트래킹) 에서 만든 vector 클래스 입니다.

다만 저는 패키지를 이용해 만들어, Physics. 이라는 접두사가 붙어 있습니다.

파이썬에서 모듈을 디렉토리 구조로 계층화 시키는 것을 패키지라고 하는데, 이전 예제 vector.py 파일이 현재 코드와 같은 위치에 있다면 from vector import vector 라고 적어야 합니다.

살펴보면 Mover 클래스는 이전 예제의 위치, 속도, 가속도 벡터를 객체 변수로 묶어놓은 클래스일 뿐입니다.

추가적으로 질량과 관계된 변수(self.mass)와 중력 상수(self.G) 가 추가되어 있음을 볼 수 있습니다.

참고로 중력상수는 1이 아니며, 이 예제에서 사용되지 않으므로 별로 신경쓸 필요가 없습니다.

applyForce() 함수는 force(힘)라는 벡터를 전달인자로 받습니다.

이 힘벡터를 A = F/M 이라는 수식을 적용시켜 질량(스칼라임에 유의)으로 나누면 가속도가 나오고 이를 Mover의 가속도 벡터에 더하는 역할을 이 함수가 수행합니다.

조금 특이한 부분이 있는데, 함수내부 force /= vector(self.mass, self.mass) 코드는 힘이라는 벡터를 질량이라는 스칼라와 연산해야 하나, 힘벡터와 질량벡터를 연산하고 있습니다.

이는 vector 클래스에 벡터와 스칼라 나누기를 구현한 함수가 없으므로 질량을 벡터로 바꾸어 연산한 것일 뿐 결과는 같습니다.

왜냐하면 벡터와 스칼라간 연산은 각 벡터의 성분마다 스칼라를 연산하면 그만이기 때문입니다.

질량은 스칼라임을 유의하시기 바랍니다.

C++이라면 force /= self.mass 로 구현해 더 간결하겠지만, 파이썬은 아쉽게도 클래스 연산자에 대한 함수 오버로딩을 아직 지원하지 않습니다.

파이썬의 함수 오버로딩 내용은 파이썬의 singledispatch를 검색해 참조하시기 바랍니다.


2. gravity.py 파일 코드

1번 파일과 2번 파일을 같은 경로에 두고 2번 gravity.py (메인함수) 를 실행하면 됩니다.

import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRectF
from PyQt5.QtGui import QPainter, QBrush

from Physics.vector import vector
from Physics.mover import Mover

from threading import Thread

import time

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)


class CWidget(QWidget):

    def __init__(self):
        super().__init__()

        #이동 물체
        self.mover = Mover()
        self.mover.location.x = self.width()/2
        self.mover.location.y = self.height()/2
        self.d = 20
        self.r = self.d/2       

        self.thread = Thread(target=self.threadFunc)
        self.bThread = False      

        self.initUI()

    def initUI(self):
        self.setWindowTitle('force')        
        self.bThread = True
        self.thread.start()
        self.show()   

    def paintEvent(self, e):
        qp = QPainter()
        qp.begin(self)        
        rect = QRectF(self.mover.location.x-self.r, self.mover.location.y-self.r, self.d, self.d)
        qp.setBrush(self.mover.color)
        qp.drawEllipse(rect)
        qp.drawText(self.rect(), Qt.AlignTop|Qt.AlignLeft, '중력이 y방향으로 +0.1만큼 적용.')
        qp.end() 

    def threadFunc(self):
        while self.bThread:
           
            gravity = vector(0.0, 0.1)
            self.mover.applyForce(gravity)                
                
            self.mover.velocity += self.mover.acceleration                
            self.mover.velocity.setLimit(10)               
            self.mover.location += self.mover.velocity

                
            if self.mover.location.x+self.r > self.width():
                self.mover.location.x = self.width()-self.r
                self.mover.velocity.x *= -1
            elif self.mover.location.x-self.r < 0:
                self.mover.velocity.x *= -1 
                self.mover.location.x = 0+self.r
            if self.mover.location.y+self.r > self.height():
                self.mover.location.y = self.height()-self.r
                self.mover.velocity.y *= -1
            elif self.mover.location.y-self.r < 0:
                self.mover.velocity.y *= -1
                self.mover.location.y = 0+self.r
            
            # 가속도를 0 설정
            # 뉴턴의 제1법칙(관성)에 따라 속도는 유지
            self.mover.acceleration*=vector(0,0)            

            self.update()

            time.sleep(0.01)
            
    def closeEvent(self, e):
        self.bThread = False
         

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    sys.exit(app.exec_())

이 코드는 이전 게시물의 mouse_tracking.py 와 거의 동일하므로 서로 다른 부분만 설명하겠습니다.

다른 점이 있다면 위치, 속도, 가속도를 각 벡터로 3개 만들어 사용하던 것을 Mover라는 클래스로 묶어서 사용하는 것이 다를 뿐입니다.

중요한 부분은 51~56번 라인의 코드만 유심히 살펴보면 되겠습니다.

바로 gravity라는 벡터를 하나 만듭니다. 근데 벡터의 y 성분이 +0.1 이므로 이 벡터는 방향은 아래로, 크기는 0.1 인 중력을 모방한 힘입니다. (힘 = 벡터)

gravity 벡터를 mover의 applyForce() 함수 전달인자로 보내 질량으로 나누어 가속도를 구합니다. (A = F/M)

그리고 이 가속도를 mover 객체의 가속도에 더하면 마치 중력의 영향으로 물체가 아래로 떨어지는것과 같은 움직임을 만들어 낼 수 있습니다.

바닥에 물체에 닿았을때 튕겨나는 이유는, 이 때 속도 벡터의 y 성분 부호를 바꾸어 주기 때문에 위로 향하게 됩니다.

다시 위로 향하는 속도 벡터의 크기는 gravity 벡터의 영향으로 y 성분이 점차 마이너스에서 플러스로 바뀌고 이 순간 위치 벡터의 y 성분이 커져 다시 아래로 향햐는 행동을 반복하게 됩니다.

다만, 아직 마찰력을 구현하지 않았기 때문에 힘이 감소되지 않아 정지하지 않고 반복되는 것입니다.

아래는 바람(x방향)과 중력(y방향)을 함께 적용해 보았습니다.



다음 포스팅에는 마찰력을 구현한 예제도 살펴 보겠습니다.



pyinstaller로 제작한 실행파일 : 벡터중력


[개발 환경]
  • 운영체제 : MS Window 10 Pro
  • 개발언어 : Python 3.7 (32bit), PyQt5 (5.11.3)
  • 개발도구 : MS Visual Studio 2017 Pro

[참고 자료]
  • Nature of Code , 다니엘 쉐프만

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)