파이썬 예제 (벡터, 마우스 트래킹)

Vector and Mouse Tracking


벡터(vector)는 무엇일까요?

C++언어를 공부한 사람이라면 벡터 (std::vector)는 가변형 배열로 생각할 수도 있습니다.

오늘 예제를 통해 공부할 벡터는 고등 수학에서 배우는 유클리드 벡터를 의미합니다.

바로 크기와 방향을 모두 갖는 물리량입니다.

무게(1kg) , 거리 (1m) 등 크기만을 가지는 양을 스칼라라고 하고, 벡터는 여기에 방향이 같이 더해진 개념입니다.

예) 집에서 40도(북동쪽) 방향으로 5km 떨어진 곳에 호수가 있다.


[벡터의 예]

프로그래밍을 공부하는데 뜬금없이 왜 벡터를 이야기하나 생각하실 수도 있지만, 우리가 즐겨하는 게임의 세계는 대부분 벡터라는 개념을 이용해 컴퓨터 세계에 창조되어 있습니다.

위치, 이동, 회전, 중력, 속도, 가속도 등의 요소가 들어간 게임이라면 틀림없이 벡터를 사용합니다.

아직 무슨 말인지 잘 이해가 안되리라 생각합니다.

먼저 오늘 만들 주제의 결과물을 한번 보시죠.



위 예제는 Python, PyQt5 을 이용해 만들었으며 벡터를 이용해 마우스를 따라오도록 만든 예제 입니다.

제가 좋아하는 책인 'Nature of Code' (저자:다니엘 쉬프만) 을 참조해 만들어 보았습니다.

해당 책에서 저자는 '프로세싱 언어'를 이용해 코드를 구현하고 있습니다.

프로세싱 언어는 C++ 언어와 문법이 유사하며, 간단한 명령을 통해 시각적인 효과를 바로 확인해 볼 수 있는 프로그래밍 언어입니다.

프로세싱 언어를 모른다고 걱정할 필요는 없습니다. C++ 또는 파이썬, 자바를 공부한 사람이라면 쉽게 이해 가능합니다.

저 또한 프로세싱 언어를 배워본적이 없지만, 내용을 이해하고 파이썬이나 C++로 적용해보는데 전혀 문제가 없었습니다.

이 글에서 벡터에 대한 수학적 개념은 설명하지 않겠습니다.

먼저 벡터에 대해 잘 모른다면 일단 예제를 실행해 보세요.

아래에 실행파일을 링크해 두었습니다.

예제를 실행해봤다면, 왜 벡터를 공부해야 하는지 필요성이 느껴지리라 생각합니다.

제가 소개한 책과 인터넷에 벡터에 대한 많은 자료가 있으므로 검색을 통해 공부해 보시기 바랍니다.

이 예제는 벡터를 이용해 속도, 가속도의 개념을 적용해 마우스를 추적하도록 만들어본 예제입니다.

[예제 실행 화면]


이제 본론으로 들어가 보겠습니다.

해당 예제는 2개의 파이썬 파일로 제작되어 있습니다.

1. 먼저 벡터를 클래스로 만든 vector.py 파일의 소스코드입니다.

import math

class vector:    

    def __init__(self, x=0.0, y=0.0, limit=9999):
        self.x = x
        self.y = y
        self.limit = limit

    def __add__(self,other):
        return vector(self.x+other.x, self.y+other.y)
 
    def __sub__(self, other):
        return vector(self.x-other.x, self.y-other.y)

    def __mul__(self, other):
        return vector(self.x*other.x, self.y*other.y)

    def __truediv__(self, other):
        return vector(self.x/other.x, self.y/other.y)

    # 벡터 내적
    def dotVector(self, other):
        return (self.x*other.x) + (self.y*other.y)

    # 벡터 외적 (3D에서 사용)
    def corssVector(self, other1, other2):
        pass

    def angleVector(self):
        # -y 윈도우 좌표계는 y가 꺼꾸로
        theta = math.atan2(0-self.y, self.x)
        deg = theta * 180.0 / math.pi;

        if deg < 0:     
            deg += 360;

        return deg

    def angleBetweenVector(self, v2):
        v = vector(self.x, self.y, self.limit)
        v.normalize()
        v2.normalize()

        theta = v.dotVector(v2)
        theta = math.acos(theta)
        deg = theta * 180.0 / math.pi
        return deg           
        

    # 벡터 정규화(방향유지, 크기 1로)
    def normalize(self):
        # 피타고라스 정의 (빗변 구하기)
        mag = math.sqrt(self.x*self.x + self.y*self.y)

        if mag > 0:
            self.x /= mag
            self.y /= mag       

    def setLimit(self, limit):
        self.limit = limit

        #copysign (x, y) y의 부호만 취해 x에 적용

        if abs(self.x) > self.limit:
            self.x = math.copysign(limit, self.x);

        if abs(self.y) > self.limit:
            self.y = math.copysign(limit, self.y)

먼저 삼각함수가 사용되므로 math 모듈을 불러옵니다.

5번 라인의 생성자 함수에서 x, y, limit라는 세가지 객체 변수를 선언합니다.

x, y는 2차원 좌표를 저장하기위한 변수이며, limit는 속도 벡터가 무한히 커져 지나치게 빨라지는 것을 방지하기 위한 변수입니다.

10~19번 라인의 특이하게 생긴 함수는 파이썬의 클래스 연산자 오버로딩입니다.

벡터끼리 연산을 처리하기 위해 더하기(__add__), 빼기(__sub__), 곱하기(__mul__), 나누기(__truediv__) 4가지 연산자 함수를 오버로딩하고 있습니다.

[벡터의 덧셈]

벡터의 연산은 쉽습니다. 같은 성분끼리(x는 x', y는 y') 서로 연산해주면 됩니다.

위 그림에서 v1(3, 4)라는 벡터와 v2(4, 1)라는 두 벡터는 v1+v2 = (7, 5) 라는 벡터로 만들어 집니다.

예) v1의 x 성분 3 + v2의 x 성분 4 = 7
예) v1의 y 성분 4 + v2의 x 성분 1 = 5

빼기, 곱하기, 나누기도 동일합니다.

위 벡터클래스를 이용해 파이썬 코드로 구현한다면 아래와 같습니다.

v1 = vector(3,4)
v2 = vector(4,1)
v3 = v1+v2

v3는 x:7, y:5 라는 벡터로 만들어 지게 됩니다.

연산자 오버로딩을 통해 두 벡터를 더하는 '+' 연산자를 __add__(self, other) 라는 함수를 이용해 구현한 것입니다. 즉 벡터끼리 더하는 행위를 하면 __add__()함수가 호출되는 원리입니다.

여기서 함수 전달인자 self는 v3 = v1 + v2 라는 코드에서 v1을 의미하고 other는 v2를 의미하게 됩니다.

여기서 한가지 파이썬의 아쉬운 점이 있는데 두 벡터끼리 더하는 연산은 __add__(self, other)로 구현이 가능하나, 벡터는 스칼라와 연산할 수 도 있는데 (방향 변경 X, 크기변경 O) , 이 경우 비컴파일 방식 언어의 특성상 함수 오버로딩을 구현하기가 쉽지 않았습니다.

파이썬 3.4 버젼에 추가된 singledispatch 데코레이터를 통해 시도해 보았으나, 클래스 멤버함수에는 적용되지 않네요.

아쉽지만 3.8버전 'functools.singledispatchmethod' 를 통해 method, class method, static method 등에 적용가능하다고 하니 기다려보겠습니다.

계속해서 23번 라인은 벡터의 내적(Dot Product)을 구하는 함수입니다.

벡터의 내적은 각 성분을 곱해서 서로 더해주면 됩니다. 유의할 점은 결과가 벡터가 아닌 스칼라로 나온다는 점입니다.

27번 라인은 벡터의 외적(Cross Product)을 구하는 함수인데, 2차원에서 의미 없으므로 생략하였습니다.

30번 라인은 수평축으로 부터 벡터의 각도를 구하는 함수입니다.


tanθ = y/x 이므로, tan역함수를 구하는 atan함수를 통해 θ를 구할 수 있습니다.

여기서 θ는 라디안 단위이므로 180을 곱하고 pi로 나누어 degree를 구합니다.

40번 라인은 두 벡터간 사이각을 구하는 함수입니다.

먼저 벡터의 방향 성분은 유지하고 크기를 1로 만드는 정규화 과정(단위벡터)을 거칩니다.

두 벡터를 내적한 스칼라값 =  cosθ 이므로 cos역함수 acos를 이용해 θ 를 구합니다. 이를 각도로 변환하는 과정은 위와 동일합니다.

다만 이렇게 구해진 각도는 0~180 도 절대각임을 유의해야 합니다.

52번 라인은 벡터를 단위벡터로 만드는 함수입니다.

단위벡터는 방향은 유지하고 벡터의 크기를 1로 만드는 것입니다.


[벡터의 크기 구하기]

먼저 다음과 같은 직각 삼각형에서 벡터의 x성분은 밑변,  y성분이 높이라면 빗변(벡터 크기)을 구하는 방법은 피타고라스의 정리를 이용하면 됩니다.


[피타고라스의 정리]

이를 이용해 벡터의 크기를 알아낼 수 있겠죠.

아직 끝이 아닙니다. 우리는 벡터의 크기(빗변)를 1로 만들어야 하므로 얻어진 빗변을 이용해 벡터의 x, y 성분을 나누어 줍니다.


[벡터 정규화]

잘 구해진 건지 확인하려면 변경된 벡터의 x, y 성분을 각각 제곱한 후 두 수를 더하면 빗변이 1이 나와야 합니다.

60번 라인은 차후 벡터를 이용해 가속도를 구현하는 경우 속도에 가속도가 누적되어 지나치게 위치가 빨리 변하는 것을 막아주는 부분입니다.

여기서 copysign(x,y)함수는 y인자의 부호만 취해 x인자에 적용(부호만)하는 역할을 수행합니다.

즉, 벡터(주로 속도벡터)의 x,y 성분이 limit보다 커지는 경우, x, y 성분을 limit로 제한하게 되는데, 이때 x성분이 -5라면 limit(제한)도 -5 (부호를 맞추어)로 걸기 위함입니다.



2. 이제 윈도우 창을 만드는 mouse_tracking.py 파일의 소스코드입니다.

앞서 만든 벡터 클래스는 벡터를 클래스화 시켰을뿐 윈도우 창을 띄우거나, 그림을 그리는 행위는 전혀 수행하지 않습니다.

바로 아래의 코드가 그 역할을 담당합니다.

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

from vector import vector
from threading import Thread
import time

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

class CWidget(QWidget):

    def __init__(self):
        super().__init__()
        #위치벡터
        self.location = vector(self.width()/2, self.height()/2)
        #속도벡터
        self.velocity = vector()
        #가속도벡터
        self.acceleration = vector()
        #마우스 좌표
        self.pt = vector(self.width()/2, self.height()/2)
        self.d = 50
        self.r = self.d/2

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

        self.initUI()

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

    def mouseMoveEvent(self, e):
        self.pt.x = e.x()
        self.pt.y = e.y()

    def paintEvent(self, e):
        qp = QPainter()
        qp.begin(self)
        rect = QRectF(self.location.x-self.r, self.location.y-self.r, self.d, self.d)
        qp.drawEllipse(rect)     

        self.showInfo(qp)

        qp.end()

    def showInfo(self, qp):
        pos = 'Position\t:X:{0:0.2f} Y:{1:0.2f}'.format(self.location.x, self.location.y)
        mousepos = 'Mouse\t:X:{0:0.2f} Y:{1:0.2f}'.format(self.pt.x, self.pt.y)
        velocity = 'Velocity\t:X:{0:0.2f} Y:{1:0.2f}'.format(self.velocity.x, self.velocity.y)
        accel = 'Accel\t:X:{0:0.2f} Y:{1:0.2f}'.format(self.acceleration.x, self.acceleration.y)
        text = pos+'\n'+mousepos+'\n'+velocity+'\n'+accel

        qp.drawText(self.rect(), Qt.AlignLeft|Qt.AlignTop|Qt.TextExpandTabs, text)

    def threadFunc(self):
        while self.bThread:

            # 현재 위치에서 마우스를 향하는 벡터를 계산
            self.acceleration = self.pt - self.location
            # 벡터길이를 정규화(너무빠른 가속도때문)
            self.acceleration.normalize()
            # 적당한 벡터의 길이로 변경 (벡터곱)
            self.acceleration *= vector(0.5, 0.5)

            #가속도는 속도에 영향
            self.velocity += self.acceleration
            #최대 속도 제한
            self.velocity.setLimit(5)
            #속도는 위치에 영향
            self.location += self.velocity            

            #화면끝에 닿으면 튕기기
            if self.location.x+self.r > self.width() or self.location.x-self.r < 0:
                self.velocity.x *= -1
            if self.location.y+self.r > self.height() or self.location.y-self.r < 0:
                self.velocity.y *= -1

            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_())

1번라인부터 필요한 모듈을 불러오는 import 구문이 나옵니다.

12번 라인부터 CWidget이라는 클래스를 생성하는 코드입니다.

PyQt5의 QWidget (일종의 윈도우 창)으로 상속받아 만들어짐을 알 수 있습니다.

14번 라인의 생성자 함수에서 앞서 우리가 만든 vector 클래스의 변수를 3개 선언합니다.

location은 현재 위치, velocity는 속도, acceleration은 가속도를 구현하는 벡터 입니다.

위치 벡터는 생성될때 윈도우 창의 중심에 위치하도록 합니다.

self가 QWidget , 즉 윈도우 창을 의미하므로 self.width(), self.height()는 윈도우 창의 너비와 높이를 가져오는 함수입니다.

pt는 마우스의 좌표를 저장할 벡터입니다.

d는 위치벡터를 이용해 그려낼 원의 지름,  r은 반지름의 크기입니다.

이어서 thread 객체와, 쓰레드에서 사용될 무한루프의 동작을 제어할 bool 타입 변수를 선언합니다.

여기까지가 생성자 함수의 역할이며, 클래스에서 사용할 변수의 선언이 주된 임무가 되겠습니다.

32번 라인은 initUI() 함수로 self.setMouseTracking(True) 함수는 마우스를 클릭하지 않아도 mouseMoveEvent() 함수가 자동으로 호출되도록 합니다.

사실 initUI()함수는 생성자에 포함시켜 함수를 만들지 않아도 무관하지만, 변수의 선언부와 윈도우 UI 관련 초기화 코드를 분리시키는 것이 좋은 설계 기법입니다.

39번 라인의 mouseMoveEvent()함수는 마우스를 이동할 때 마다 호출되며, 현재 마우스 좌표를 self.pt 객체 변수에 저장합니다.

43번 라인의 paintEvent()함수는 윈도우를 새로 그려야 할 때마다(처음 윈도우 생성시, 창 크기 변경, 윈도우 창이 가려졌다 다시 나올때) 호출되는 함수입니다.

C++ MFC의 CView Class가 제공하는 OnDraw() 함수와 유사합니다.

실제 그림을 그리는 QPainter의 변수를 선언하고 begin(), end() 함수 사이에 그려야 할 내용을 작성하면 됩니다.

바로 여기서 location 벡터 좌표를 가져와 원을 그립니다.

53번 라인의 showInfo()함수는 화면 좌상단에 표시되는 벡터의 정보를 표시하는 함수입니다. 위치, 속도, 가속도 벡터 및 마우스의 좌표 정보를 표시합니다.


62번 라인의 threadFunc()함수는 전체 코드의 가장 핵심적인 부분입니다.

별도의 실행흐름(쓰레드)으로 동작하며, 무한루프를 반복해 아래와 같은 행위를 반복적으로 수행해 위치, 속도, 가속도를 설정합니다.

  1. 마우스 위치벡터 - 현재 위치벡터 = 가속도벡터로 설정 
    현재의 위치에서 마우스를 향하는 곳의 벡터 구하기 (벡터 차연산)
  2. 가속도벡터를 정규화
    너무 큰 가속도는 X, 크기를 1로 (단위벡터)
  3. 가속도벡터를 적절한 크기로 변경
    정규화된 가속도 크기 설정 (벡터 곱연산)
  4. 속도벡터에 위에서 구해진 가속도벡터를 누적해 더하기
    속도에 가속도를 누적시켜 반복할수록 빨라지게 (벡터 합연산)
  5. 속도벡터의 최대 속도를 제한
    속도가 무한히 커지는 것을 방지
  6. 위치벡터에 위에서 얻어진 속도벡터를 누적해 더하기
    현재 위치를 속도를 이용해 변경 (벡터 합연산)
  7. 위치벡터가 화면을 벗어나는 경우 속도벡터의 부호 변경(위치 반대로)
    좌표의 부호가 바뀌면 반대로 이동하는 개념
  8. 화면을 새로 그리기
  • 가속도 : 속도가 변하는 비율
  • 속도    : 위치가 변하는 비율
1~8번 행위의 반복은 요약하자면, 가속도는 속도에 영향을 주고, 속도는 위치에 영향을 주어 차근차근 연쇄적인 반응이 일어나도록 하는 것입니다.

이제 남은 코드는 별 내용이 없습니다.

93번 라인 메인함수에서 윈도우창 클래스를 생성해 띄워서 실행시키는 것 뿐이지요.

이상으로 코드 분석을 모두 마칩니다.

아래는 pyinstaller로 제작한 실행파일 입니다.


모든 코드의 내용이 이해가 된다면 파이썬의 리스트를 활용해 여러개의 벡터 움직임도 구현해보기 바랍니다.

다음에는 힘(질량, 바람, 중력 등) 에 관한 내용을 포스팅하겠습니다.



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

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

댓글

  1. import vector 할 때, No module namesd vector라고 뜰 경우 어떻게 해야 되나요?

    답글삭제
    답글
    1. import 명령어로 모듈을 불러들일때는 다음과 같이 사용합니다.

      import xxx.py

      xxx.vector (모듈이름.vector)

      from vector import vector의 경우는 다음과 같습니다.

      vector (모듈이름없이 바로사용)

      삭제
  2. 먼저 답변이 좀 늦어 미안합니다.

    글의 vector.py 파일을 작성하고 mouse_tracking.py 파일과 같은 경로에 vector.py 파일을 넣어두면 됩니다.

    import vector로 불러들이는 경우에는 vector.vector (벡터 모듈의 vector 클래스) 라고 사용해야 합니다.

    from vector import vector는 바로 vector라고 사용가능합니다.

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

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