Map API/Kakao Map API2020. 3. 28. 21:44
 



AK플라자 분당점
AK 백화점 수원점

 

https://apis.map.kakao.com/web/guide/

 

 

Posted by cwisky
Django/JSON Response2020. 3. 18. 22:06

VaRest 플러그인을 언리얼 엔진에 설치하고 웹서버(Django)에 로그인하는 예

서버측에서 Template을 이용하지 않고 JSON 포맷으로 응답

 

users 앱 생성

python manage.py startapp users

위의 명령으로 App을 생성한 후에 생성되는 urls.py, views.py를 아래처럼 작성한다

 

users 앱 등록

프로젝트 settings.py 에 위에서 생성한 users 앱을 등록한다

INSTALLED_APPS = [
    'users',    # 새로 추가한 앱
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

 

users/urls.py

from django.urls import path 
from . import views

app_name = 'users'
urlpatterns = [
    path('', include('django.contrib.auth.urls')),
    path('register', views.register, name='register'),
    path('login_custom', views.login_custom, name='login_custom'),
]

 

users/views.py

from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.http import JsonResponse


def register(request):
    if request.method != 'POST':
        form = UserCreationForm()
    else:
        form = UserCreationForm(data=request.POST)
        if form.is_valid():
            new_user = form.save()
            login(request, new_user)
            return redirect('learning_logs:index')

    context = {'form': form}
    return render(request, 'registration/register.html', context)


def login_custom(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user:
            if user.is_active:
                login(request, user)
                print('request.user=', request.user)
            #return render(request, "users/login_result.html")
            return JsonResponse({'result': true })
        else:
            #return render(request, "users/login_result.html")
            return JsonResponse({'result': false })

 

위의 서버에 UE4 VaRest 플러그인을 사용한 로그인 ( 그림을 클릭하면 그림 전체를 볼 수 있어요~)

 

jQuery를 사용한 로그인 예

Django 서버측에서 users 앱을 생성하고 users/ajax_login 으로 요청하는 경우

 

users/urls.py

from django.urls import path, include
from . import views

app_name = 'users'
urlpatterns = [
    path('', include('django.contrib.auth.urls')),
    path('register', views.register, name='register'),
    path('login_custom', views.login_custom, name='login_custom'),
    path('login_result', views.login_result, name='login_result'),
    path('ajax_login', views.ajax_login, name='ajax_login'),
]

 

users/views.py

def ajax_login(request):
    if request.method != 'POST':
        return render(request, 'users/ajax_login.html')
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user:
            if user.is_active:
                login(request, user)
                print('request.user=', request.user)
            return JsonResponse({"result": True})
        else:
            return JsonResponse({"result": False})

 

urls/templates/users/ajax_login.html

 

<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script>
   $(document).ready(function(){

      console.log('jQuery Ready');
      $.ajax({
            url:'/users/ajax_login',
            data:{'username':'tj', 'password':'tj'},
            dataType:'json',
            method:'post',
            success:function(res) {
                  if(res.result){ alert('로그인 성공'); }
                  else alert('로그인 실패');
            },
            error : function(request, status, error){
                  console.log(error);
            }
      });
});
</script>

Posted by cwisky
Django/Setup2020. 2. 22. 19:57

Python 설치 / PATH 환경변수 설정

 

가상환경(Vertual Environment)

한개의 가상환경은 다른 가상환경과 서로 디렉토리가 다르며 파이썬과 그 라이브러리의 버전을 달리 설치할 수 있다

 

1. 가상환경을 설치할 디렉토리 생성

django_web 등의 임의의 이름을 가진 디렉토리를 생성한다

아래의 모든 명령은 언급이 없으면 위에서 생성한 디렉토리 안에서 실행한다

 

CMD를 이용하여 특정 디렉토리로 이동하여 그 곳에 가상환경을 생성하는 명령은 다음과 같다.

python -m venv venv01    /* venv01 은 가상환경의 이름이며 해당 디렉토리가 생성된다 */

 

 

2. 가상환경의 활성화

source venv01/bin/activate

가상환경이 활성화된 후에라야 해당 가상환경에 패키지를 설치할 수 있고 가상환경에 이미 설치된 파이썬 패키지를 사용할 수가 있다

윈도우 환경에서는 'source' 를 빼고 그냥 'venv01\Scripts\activate' 명령을 사용하고,

Power Shell 을 사용한다면 Activate 첫자는 대문자('A')를 사용해야 한다

가상환경이 활성화되면 아래처럼 괄호 안에 가상환경의 이름이 표시된다

(venv01) c:\Python_Projects>  

가상환경이 활성화된 경우에만 Django 를 사용할 수 있다

 

 

필요시 가상환경의 비활성화 명령

deactivate

커맨드창을 닫으면 가상환경도 비활성화된다

 

 

3. Django 설치

가상환경이 생성되고 활성화된 디렉토리 안에서 다음과 같은 명령으로 해당 가상환경에 Django를 설치할 수 있다.

장고 2.2 버전에 맞는 mysqlclient 가 아직 없기 때문에 장고 2.1버전을 설치한다(2020.03.02현재 장고 2.2 와 mysqlclient를 연결하는 과정에서 아래와 같은 오류가 발생함)

django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.13 or newer is required; you have 0.9.3.

pip install django <- django 최신버전이 설치되어 mysqlclient 현재버전과 연결할 때 오류발생

 

MySQL 8 버전과 호환되는 Django 2.1을 설치할 때는 아래처럼 버전을 지정할 수 있다

pip install django==2.1

 

 

4. Django 프로젝트 생성

django-admin startproject myproj .

마지막에 있는 점(.)을 빠뜨리지 않도록 한다. 만약 빠뜨린 경우에는 가상한경 디렉토리만 남기도 모두 삭제한 후에 다시 명령을 실행한다

 

 

MySQL 설정

디폴트로 제공되는 SQLite3 데이터베이스를 사용하지 않고 MySQL 을 사용하려는 경우에는 아래처럼 변경한다

윈도우용 MySQL Client 설치(사용하는 Python 버전에 따라 적당한 설치파일 선택)

MySQL 8, Python 3.8, Django 2.1 환경일 때 아래의 방법으로 문제 없이 성공함

 

윈도우용 mysqlclient 다운로드(cp38: 파이썬 3.8의미, 설치된 Python이 32비트인지 64비트인지 확인하여 해당파일 사용). mysqlclient 설치에 실패하면 64비트 버전과 32비트 버전을 변경하여 설치하면 성공하는 경우도 있다고 함

 

 

4. 설치된 Python의 몇 비트인지 확인하는 Python명령

>>> import platform 
>>> print(platform.architecture())

 

 

5. 미리 컴파일된 mysqlclient 다운로드

https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysql-python

 

 

6. MySQL Client 설치

Downloads 폴더에 다운로드했다면, 해당 폴더로 이동하여

pip install mysqlclient-1.4.6-cp38-cp38-win32.whl

 

 

7. pymysql 모듈 설치

pymysql 모듈을 사용하는 경우

pip install pymysql

프로젝트 폴더 안의 settings.py를 열고 ...

 

 

8. settings.py 파일 내용의 맨 위에 다음과 같이 추가한다

import pymysql 
pymysql.install_as_MySQLdb()

 

 

9. settings.py 중간 약간 아랫쪽에서 다음과 같은 내용을 찾아서 아래처럼 변경한다

# Database 
https://docs.djangoproject.com/en/3.0/ref/settings/#databases 
# 디폴트 상태는 아래와 같다
DATABASES = { 
    'default': { 
        'ENGINE': 'django.db.backends.sqlite3', 
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 
    } 
}

 

# MySQL을 사용하려면 위의 내용을 아래처럼 해당 데이터베이스 연결정보로 변경한다

DATABASES = { 
    'default': { 
        'ENGINE': 'django.db.backends.mysql', 
        'NAME': 'testdb', 
        'USER': 'root', 
        'PASSWORD': 'a1234', 
        'HOST': 'localhost', 
        'PORT': '3306',
    } 
}

 

 

10. cryptography 설치

pip install cryptography

 

 

11. 프로젝트를 관리할 데이터베이스 생성

python manage.py migrate

settings.py 에 설정된 데이터베이스가 생성된다

위의 과정 중에 아래처럼 오류가 발생할 수 있다

RuntimeError: cryptography is required for sha256_password or caching_sha2_password

위의 오류는 cryptography 모듈이 설치되지 않았기 때문에 발생하므로 아래처럼 설치한 다음, 위의 명령을 다시 실행한다

 

 

 

데이터베이스 생성에 성공하면 MySQL 의 지정된 DB(여기서는 testdb) 안에 10개 정도의 테이블이 생성된 것을 확인할 수 있다(MySQL Workbench 등에서 쉽게 확인)

 

 

12. 내장된 웹서버를 실행하여 프로젝트 작동상태를 웹브라우저로 확인

python manage.py runserver

(venv01) c:\Python_Projects>python manage.py runserver 
Watching for file changes with StatReloader 
Performing system checks... 

System check identified no issues (0 silenced). 
February 22, 2020 - 19:39:13 
Django version 3.0.3, using settings 'myproj.settings' 
Starting development server at http://127.0.0.1:8000/ 
Quit the server with CTRL-BREAK.

CTRL-C 를 사용해도 웹서버를 중지할 수 있다

 

 

13. 웹브라우저로 접속

http://localhost:8000

웹브라우저에 위의 URL에 접속해보면 로켓트와 아래의 문장이 보이면 성공한 것이다

The install worked successfully! Congratulations!

 

포트번호 변경

에러나 접속이 되지 않는 경우에는 8000 번 포트번호를 이미 다른 프로그램에서 사용 중일 경우가 많다. 다음과 같은 명령을 사용하여 포트번호를 변경할 수 있다

python manage.py runserver 8001

 

 

Posted by cwisky
Networking/Server Travel2020. 2. 20. 17:17

로비맵에 현제 접속 중인 모든 클라이언트들에게 동시에 게임맵으로 전환(Server Travel)하고 로비맵에서 선택한 캐릭터를 게임맵에서 스폰하는 예

 

로그인 버튼을 누르면 서버측(GameMode)에서 로그인 순서대로 플레이어에게 순번을 부여하면 클라이언트 측의 SaveGame이나 GameInstance 에 저장했다가, 2명이 모두 로그인하면 서버측(GameMode)에서는 Server Travel 명령을 실행하여 모두 클라이언트가 ThirdPersonExampleMap 으로 전환하도록 한다.

Server Travel 후에 실행되는 ThirdPersonGameMode의 OnRestartPlayer 함수에서 SaveGame 이나 GameInstance 로부터 플레이어의 순번을 참조하여 해당 번호의 캐릭터를 스폰하여 3인칭 맵 위에 표시한다

 

 

언리얼 에디터에서 3인칭 프로젝트 템플릿을 사용하여 프로젝트 생성

 

 

2개의 캐릭터 생성

  • ThirdPersonCharacter : 디폴트로 존재하는 캐릭터
  • BP_Blue : ThirdPersonCharacter를 복제하여 BodyColor 만 변경한 캐릭터

 

복제된 캐릭터의 BodyColor 를 변경하기 위해 아래의 Construction Script 참고

 

2개의 레벨 생성(아무것도 없는 Empty 레벨, 3인칭 템플릿 레벨)

  • LobbyMap : 아무것도 없는 로비용 Empty 레벨
  • ThirdPersonExampleMap  : 3인칭 프로젝트 템플릿에 포함된 게임용 레벨
  • 게임맵을 더블클릭하여 에디터에서 열고 월드아웃라이너에서 디폴트로 포함된 Network Player Start를 삭제한다
  • 언리얼 에디터 > 모드 > 배치 >검색창에 Player Start 를 검색하여 드래그하여 레벨에 필요한 플레이어 수만큼 배치한다

 

2개의 GameMode 생성

  • GM_Lobby : Use Seamless Travel 속성 체크
  • 로비맵의 GameMode에디터에서 Default Pawn Class 항목에 None 설정
  • ThirdPersonGameMode (디폴트 생성됨) : Use Seamless Travel 속성 체크
  • 게임맵의 GameMode에디터에서 Default Pawn Class 항목에 Pawn 설정

 

2개의 PlayerController 생성

  • BP_LobbyPlayerController (로비맵에서 사용될 PlayerController)
  • BP_ThirdPersonPlayerController (게임맵에서 사용될 PlayerController)
  • 프로젝트 세팅 항목에서 Editor Startup Map, Game Default MapLobbyMap으로 설정한다

 

SaveGame 블루프린트 클래스 생성(SG_PlayerNumber)

  • Player Number 정수 변수 선언

 

GameInstance 클래스 생성(BP_GameInstance)

  • 여기에서는 GameInstance에 Player Number 정수 값을 저장하여 레벨전환 후에 다시 참조할 것이므로 Player Number 라는 정수변수 1개만 선언한다
  • 프로젝트 세팅 > 맵 & 모드 > Game Instance 섹션의 Game Instance Class항목에 등록한다

 

플레이 설정

  • 언리얼 에디터 > 플레이 버튼 우측 역삼각형 > 고급세팅 > Multiplayer Options > 단일 프로세스 항목 체크해제, 에디터 멀티플레이어 모드 항목에 Play As Client 설정
  • 언리얼 에디터 > 플레이 버튼 우측 역삼각형 > 플레이어 수: 2,    독립형 게임 선택
  • 위와 같이 설정한 후에는 [플레이] 버튼만 누르면 위의 설정대로 실행된다
  • 게임이 실행되면 2개의 윈도우에 각 플레이어의 뷰가 표시되고 키보드와 마우스로 각 캐릭터를 제어할 수 있다
  • ALT + TAB 키를 누르면 제어대상 윈도우를 변경할 수 있다
  • 게임 윈도우의 종료는 ALT + TAB 키를 눌렀을 때 보여지는 섬네일 윈도우의 X 버튼을 누르면 된다
  • 한대의 컴퓨터에서 테스트하면 캐릭터가 각 플레이어마다 동일하게 스폰되므로 2대의 컴퓨터에서 테스트해야 한다
  • 패키징하여 네트워크 환경에서 서버와 클라이언트를 실행하는 방법은 여기를 참조하세요

 

WG_Login 위젯생성 (로비맵의 화면에 표시될 로그인버튼 한개만 필요)

 

 

로비맵에서 사용될 PlayerController

 

 

LobbyPlayerController의 멤버변수 Player Number변수복제 노티파이 함수의 내용

 

 

로비맵의 게임모드에서 플레이어 2명이 로그인하면 Server Travel 명령을 실행하여 게임맵(ThirdPersonExampleMap)으로 모든 접속된 클라이언트들을 전환한다

 

 

로비용 PlayerController 에서 Player Number 변수를 선언하고 변수복제(Rep Notify)를 설정한다

플레이어가 로그인 버튼을 누른면 서버측에서 로그인 순번을 지정하여 Player Number 변수에 저장하므로 Player Number 변수는 변수복제를 사용하여 클라이언트에게 복제되도록 하고 노티파이 함수(On Rep Player Number)도 실행되도록 한다. 변수가 복제되면 SaveGame 을 이용하여 Player Number값을 디스크에 저장한다.

이 값은 나중에 게임맵으로 전환될 때 게임맵의 게임모드에서 읽어들여서 해당 번호의 캐릭터를 스폰하는데 사용한다

 

 

위와 동일하지만 SaveGame이 아닌 GameInstance에 Player Number 를 저장하는 경우

 

 

게임맵의 GameMode에서는 우선 Choose Player Start 를 오버라이드하여 Player Start 위치에 캐릭터가 생성되도록 준비한다

 

 

게임맵의 GameMode에서 각 플레이어에게 캐릭터를 스폰하여 표시해줄 책임이 있으므로 아래처럼 다양한 방법으로 이전에 선택된 캐릭터를 생성한다. 여기서는 OnRestartPlayer 이벤트 함수를 오버라이드 한 예이다. 이 외에도 Spawn Default Pawn at Transform 이나 Spawn Default Pawn For 를 오버라이드하면 더 간편하게 스폰할 수 있다. 이 페이지 하단에 이들 2개 함수의 사용 예를 추가했다

위에서 사용된 Load Player Number 함수는 게임맵의 PlayerController에 정의된 커스텀 이벤트 함수이며 이 함수는 내부에서 다시 GameMode의 RequestPossess 함수를 호출할 때 자신의 Player Number를 파라미터로 전달한다. Player Number를 받은 GameMode 에서는 해당 번호의 폰을 스폰하여 Possess함수를 호출하는 내용이다

 

 

게임맵에서 사용되는 PlayerController의 전체 내용

 

 

게임맵 GameMode 의 변수 섹션에 폰 클래스 배열을 생성한다

 

 

Server Travel 실행 후 로드되는 게임맵에서 폰을 스폰하는 3가지 방법

  • Server Travel 으로 이동된 맵의 GameMode::OnPostLogin 은 실행되지 않으므로 OnRestartPlayer를 오버로드한다
  • OnRestartPlayer를 오버로드할 때는 스폰 후에 Possess함수를 호출해야 한다
  • OnRestartPlayer를 사용하지 않으려면 Spawn Default Pawn at Transform 이나 Spawn Default Pawn For 를 오버라이드하면 된다. 2개의 함수 모두 OnRestartPlayer보다 간편하고 Possess를 호출할 필요가 없다.
  • OnRestartPlayer 실행 후에 Spawn Default Pawn at Transform 이나 Spawn Default Pawn For 가 자동으로 호출된다. 그러므로 이들 3개 중 한개의 함수만 오버로드하여 폰을 스폰해주면 된다

 

OnRestartPlayer 를 오버로드하여 게임맵에 스폰하는 예

 

게임맵의 GameMode에서 Spawn Default Pawn at Transform 오버로드(GameInstance이용하는 버전)

Spawn Default Pawn at Transform 은 OnRestartPlayer 이후에 자동으로 호출되며 Possess 함수는 사용할 필요 없다

 

위와 동일한 함수이며 SaveGame을 이용하는 버전

 

Spawn Default Pawn For 를 오버로드하여 폰을 생성하는 예

Spawn Default Pawn For는 OnRestartPlayer 이후에 자동으로 호출되며 Possess 함수는 사용할 필요 없다

 

Posted by cwisky

Visual Studio Code를 실행하고 왼쪽 EXPLORER 에서 작업폴더를 선택하고 CTRL + SHIFT + P 키를 누른다

검색창에 Configure Task 입력

Create tasks.json file from template 항목 선택

MSBuild, maven, .NetCore, Others 등의 아이템 중 Others 선택 : 이 결과 tasks.json  파일이 작업폴더에 생성됨

 

생성된 tasks.json의 파일의 내용

{

   // See https://go.microsoft.com/fwlink/?LinkId=733558

  // for the documentation about the tasks.json format

  "version": "2.0.0",

  "tasks": [

        {

          "label": "echo",

          "type": "shell",

          "command": "echo Hello"

       }

  ]

}

 

 

tasks.json 파일의 내용을 아래처럼 변경하여 저장한다

{

   "version": "2.0.0",

   "tasks": [

      {

         "label": "Chrome",

         "type": "process",

         "command": "chrome.exe",

         "windows": {

            "command": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"

         },

         "args": [

            "${file}"

         ],

         "problemMatcher": [],

         "group": {

            "kind": "build",

            "isDefault": true

         }       

       }

   ]

}

 

 

VS Code에서 프런트 엔드 코드(HTML, CSS, Javascript)를 작성하고 CTRL + SHIFT + B 키를 누르면 구글 Chrome이 실행되어 프런트엔드 코드가 크롬에서 실행되는 것을 확인할 수 있다

Posted by cwisky

mainwindow.h

 

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include  <QMainWindow>
#include  <QTimerEvent>
#include  <QPaintEvent>

class MainWindow : public QMainWindow
{
    Q_OBJECT
    QPolygon *polygon;
    QPointF pts[4];

public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();

    void paintEvent(QPaintEvent* e);
    void timerEvent(QTimerEvent *e);
    void rotateRect(float a);
};

#endif // MAINWINDOW_H

 

 

mainwindow.cpp

 

#include "mainwindow.h"
#include  <QPainter>
#include  <QPolygon>
#include  <cmath>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    //원점을 중심으로 위치한 정사각형의 꼭지점 좌표
    pts[0] = QPointF(-50,-50);
    pts[1] = QPointF(50,-50);
    pts[2] = QPointF(50,50);
    pts[3] = QPointF(-50,50);

    polygon = new QPolygon(4); // 4개의 꼭지점을 갖는 다각형
    for(int i=0;i<4;i++){
        polygon->setPoint(i,pts[i].x(),pts[i].y());
    }

    startTimer(50);
}

MainWindow::~MainWindow(){}

void MainWindow::paintEvent(QPaintEvent *e)
{
    QPainter painter(this);
    rotateRect(0.1F);
    painter.setBrush(QBrush("#ff0000"));
    painter.drawPolygon(*polygon);
}

void MainWindow::timerEvent(QTimerEvent *e)
{
    repaint();
}

void MainWindow::rotateRect(float a)
{
    /* 행렬을 이용한 점(벡터)의 회전(원점을 중심으로 Θ라디안 회전)
     cos(Θ), -sin(Θ)        px
     sin(Θ), cos(Θ)     *   py
     -------------------------
     x' = cos(Θ)*px - sin(Θ)*py
     y' = sin(Θ)*px + cos(Θ)*py
    */

    for(int i=0;i<4;i++){
        float x = cos(a)*pts[i].x() - sin(a)*pts[i].y();
        float y = sin(a)*pts[i].x() + cos(a)*pts[i].y();
        pts[i].setX(x);
        pts[i].setY(y);
        polygon->setPoint(i,
           pts[i].x()+width()/2,
           pts[i].y()+height()/2
        );
    }
}

 

 

main.cpp

 

#include "mainwindow.h"
#include  <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;

    w.resize(550, 450);
    w.setWindowTitle(QString::fromLocal8Bit("행렬을 이용한 회전"));
    w.show();

    return a.exec();
}

Posted by cwisky

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

 

struct Weapon
{
   string name;
   int power;
   int max_use;
   Weapon() {}
   Weapon(string name, int power, int max_use)
      :name(name), power(power), max_use(max_use) {}
   void print() const {
      cout << name << "\t" << power << "\t" << max_use << endl;
   }
};

template  <typename T>
struct SortPower
{
   bool operator()(const T& l, const T& r) const {
      return l.power < r.power;
   }
};

 

template  <typename T>
struct BinWSearch
{
   bool operator()(const T& w1, const T& w2) const{
      cout << w1.power << ", " << w2.power << endl;
      return w1.powerw2.power;
   }
};

 

int main()
{

   vector wVec;
   wVec.push_back(Weapon("Sword   ", 45, 50));
   wVec.push_back(Weapon("X-Caliber", 64, 80));
   wVec.push_back(Weapon("Bow     ", 72, 20));
   wVec.push_back(Weapon("Revolver", 87, 20));
   wVec.push_back(Weapon("Shotgun ", 92, 10));

   sort(wVec.begin(), wVec.end(), SortPower());
   for (auto iter = wVec.begin(); iter != wVec.end(); iter++) {  //정렬상태 확인
      iter->print();
   }

   bool found = binary_search(wVec.begin(), wVec.end(), 
   Weapon("X-Caliber", 64, 80), BinWSearch());

   cout << (found ? "검색성공" : "검색실패") << endl;

   cout << "프로그램 종료" << endl;
   return 0;
}

Posted by cwisky

사원수의 기본적인 형태

q = [ w, xi, yj, zk ]

 - w : scalar

 - xi, yj, zk : 허수로 표현된 벡터

 

스칼라를 제외한 허수들은 벡터를 난타낸다. 그러므로 아래처럼 간략하게 나타내기도 한다

q = [ w, v ]

 

단위 사원수

q = [ 1, 0, 0, 0 ]

 

단위 사원수의 경우, 절대값(크기)이 1이다.

|| q || = sqrt(w^2 + x^2 + y^2 + z^2) = 1,  w^2 + x^2 + y^2 + z^2 = 1

 

사원수 크기 ( Length, Norm)

|| q || = Norm(q) = sqrt(w^2 + x^2 + y^2 + z^2)

정규화(단위사원수 생성) : 사원수의 크기로 사원수를 나눈다

q = q / || q || = q / sqrt(w^2 + x^2 + y^2 + z^2)

 

켤레 사원수

[ w, xi, yj, zk ]의 켤레 사원수는 [ w, -xi, -yj, -zk ]

 

사원수의 역수

사원수 q의 역수 = (켤레 사원수) / || q ||^2, 그러므로 크기가 1인 경우, 역수=켤레사원수

단위 사원수의 역수 = 켤레 사원수 (단위사원수는 크기가 1이므로 )

 

회전을 위한 사원수(회전축, 회전량을 표현함)

q = [ cos(θ/2), xsin(θ/2), ysin(θ/2), zsin(θ/2) ] = [ cos(θ/2), v*(θ/2) ]

 - x, y, z : 회원축을 나타내는 단위벡터(v)

 - θ : 회전량(Radian)

 - 이렇게 설정했을 때 단위 사원수가 됨

 

점(벡터) 회전을 위한 곱셈

q´ = q v q-1, (v = [0, v])

q' : 벡터 v가 회전변환된 결과 벡터를 포함한 사원수

q : 회전을 위한 사원수(회전축과 회전량 포함)

v : 회전 대상 점(벡터)을 사원수 형식으로 표현 ( v = [0, v] ), 일반벡터 앞에 0을 추가한 것

q-1 : 사원수 q의 역수(단위 사원수의 역수는 켤레 사원수와 같다)

 

사원수의 곱셈규칙

q1=(w1, x1, y1, z1);        q2=(w2, x2, y2, z2);

 

위의 각 사원수에서 벡터부를 아래처럼 표현할 수 있다

v1 = (x1, y1, z1),             v2 = ( x2, y2, z2 )


q1 * q2 = ( w1*w2 - v1.Dot(v2),   v2*w1 + v1*w2 + v1.Cross(v2 ) )

 

위의 식을 가감승제로만 표현하면 다음과 같다

w = w1w2 - x1x2 - y1y2 - z1z2

x = w1x2 + x1w2 + y1z2 - z1y2

y = w1y2 + y1w2 + z1x2 - x1z2

z = w1z2 + z1w2 + x1y2 - y1x2

 

참고 : 벡터의 외적(Cross Products)

사원수 나눗셈 규칙

https://kr.mathworks.com/help/aeroblks/quaterniondivision.html

The Quaternion Division block divides a given quaternion by another.

The quaternions have the form of

q=q0+iq1+jq2+kq3

and

r=r0+ir1+jr2+kr3.

The resulting quaternion from the division has the form of

t=qr=t0+it1+jt2+kt3,

 

사원수를 이용하여 공간상의 특정 점(벡터)을 회전하는 예

#include <iostream>
#include <cmath>

using namespace std;

struct Vector
{
	float x, y, z;
	Vector() {}
	Vector(float x, float y, float z) :x(x), y(y), z(z) {}
	/* Dot Product
		v = [x, y, z]
		v'= [x',y',z']
		Dot(v) = xx' + yy' + zz'
	*/
	float Dot(const Vector& v2) const {
		return x * v2.x + y * v2.y + z * v2.z;
	}
	/* Cross Product
		v = [x, y, z]
		v'= [x',y',z']
		v x v' = [yz'-zy', zx'-xz', xy'-yx']
	*/
	Vector Cross(const Vector& v2) const {
		return Vector(
			y*v2.z - z * v2.y,
			z*v2.x - x * v2.z,
			x*v2.y - y * v2.x);
	}
	Vector operator*(float scalar) const {
		return Vector(x*scalar, y*scalar, z*scalar);
	}
	Vector operator+(const Vector& v2) const {
		return Vector(x + v2.x, y + v2.y, z + v2.z);
	}
	float length() {
		return sqrt(x*x + y * y + z * z);
	}
};

struct Quaternion
{
	float w;
	Vector v;
	Quaternion() {}
	Quaternion(float w, float x, float y, float z)
		:w(w), v(Vector(x, y, z)) {}
	Quaternion(const Vector& n, float a)
	{
		a = 3.14159F / 180.0F * a;
		w = cos(a / 2);
		v.x = n.x*sin(a / 2);
		v.y = n.y*sin(a / 2);
		v.z = n.z*sin(a / 2);
	}
	Quaternion(float w, Vector v) : w(w), v(v) {}
	/*
	q1 * q2 = (w1,v1) * (w2,v2) =
	( w1w2-v1.Dot(v2), w1v2 + w2v1 + v1.Cross(v2) )
	*/
	Quaternion operator*(const Quaternion& q) const {
		Quaternion r;
		r.w = w * q.w - v.Dot(q.v);
		r.v = q.v*w + v * q.w + v.Cross(q.v);
		return r;
	}
	const Vector rotate(const Vector& V) const {
		Quaternion p;
		p.w = 0;
		p.v = V;

		const Quaternion& q = (*this);
		return (q * p * q.Invert()).v;
	}
	Quaternion Invert() const {
		//단위 쿼터니언의 역수는 켤레 사원수와 같으므로
		return Quaternion(w, Vector(-v.x, -v.y, -v.z));
	}
	float magnitude() {
		return sqrt(w*w + v.x*v.x + v.y*v.y + v.z*v.z);
	}
};

int main()
{
	//Quaternion(사원수)
	Quaternion q1(Vector(1, 0, 0), 90);
	Quaternion q2(Vector(0, 1, 0), 90);
	Quaternion q3 = q1 * q2;

	cout << "w:" << q2.w << ", x:" << q2.v.x << ", y" << q2.v.y << ", z:" << q2.v.z << endl;
	cout << "크기:" << q2.magnitude() << endl;

	Vector v1(0, 0, 1);
	Vector v2 = q1.rotate(v1); //x축 중심 90도 회전
	cout << "v2, x:" << v2.x << ", y:" << v2.y << ", z:" << v2.z << endl; // y:-1
	cout << "v2 결과벡터 크기:" << v2.length() << endl; // 1

	Vector v3 = q2.rotate(v1); //y축 중심 90도 회전
	cout << "v3, x:" << v3.x << ", y:" << v3.y << ", z:" << v3.z << endl; // x:1
	cout << "v3 결과벡터 크기:" << v3.length() << endl; // 1

	/*
	Quaternion q(Vector(1, 0, 0), 90);
	cout << "크기:" << q.magnitude() << endl; //1
	*/

	return 0;
}

위의 코드를 실행하면 다음과 같은 결과를 볼 수 있다

w:0.707107, x:0, y0.707106, z:0 
크기:1 
v2, x:0, y:-1, z:1.19209e-06 
v2 결과벡터 크기:1 
v3, x:1, y:0, z:1.19209e-06 
v3 결과벡터 크기:1

 

Posted by cwisky

언리얼 엔진에서 휴머노이드 캐릭터간의 애니메이션 Retargeting


스켈레탈 애니메이션은 뼈대를 기반으로 하는 애니메이션이므로 원칙적으로 서로다른 골격구조를 가진 캐릭터의 애니메이션은 서로 교환하여 사용할 수가 없다. 

언리얼 엔진에서는 Animation Retargeting 이라는 방법을 사용하여 휴머노이드 캐릭터 간에는 골격구조가 서로 다르더라도 Rig 애셋을 통하여 애니메이션을 서로 교환하여 사용할 수 있다

언리얼 엔진은 두 캐릭터 간에 서로 뼈대정보를 공유하기 위해 새로운 애셋인 휴머노이드 Rig 애셋을 생성하여 사용하는 방법을 제공한다


여기서는 mixamo 사이트에서 다운로드한 캐릭터와 애니메이션을 언리얼 프로젝트로 임포트하여 언리얼 엔진에서 제공하는 마네킹 캐릭터에 사용되는 애니메이션을 mixamo 에서 가져온 캐릭터에 적용하는 절차에 대해 알아 보고자 한다. 

mixamo.com 사이트에서 캐릭터와 애니메이션을 다운로드하고 언리얼 프로젝트로 임포트하는 방법은 다음 링크를 눌러 참조하면 된다

https://unrealengine.tistory.com/category/Skeletal%20Animation/Mixamo%20Character



설정 절차 ( 마네킹 캐릭터의 애니메이션을 mixamo 캐릭터에게 설정하는 예)

Skeleton Asset 더블클릭하여 에디터 열기


1. 소스 스켈레톤에 Rig 애셋 생성하기

Content Browser에서 마네킹 스켈레톤을 더블클릭하여 페르소나 에디터를 열고 

툴바 > Retarget Manager > Select Rig: Select Humanoid Rig   

위와 같이 선택하면 자동으로 Rig 이름과 뼈의 이름이 서로 나란히 매칭된 표가 표시된다  언리얼 캐릭터의 경우에는 자동 매핑이 문제 없지만 외부에서 임포트한 다른 스켈레톤에는 적절치 않는 매핑이 될 수 있으므로 자동으로 설정하더라도 반드시 모든 본이 적절히 설정되었는지 확인해야 한다


2. 소스 Rig 애셋 저장

스켈레톤 에디터의 툴바에서 Save 버튼을 눌러 생성된 휴머노이드 Rig애셋 정보를 저장한다

설정된 Rig 정보를 별도의 파일에 저장하려면 중간에 있는 [저장] 버튼을 누르면 된다. 저장된 매핑 정보는 다음에 동일한 작업을 할 때 일일이 설정하는 것이 아니라 [로드] 버튼을 눌러 적용하면 된다


3. 타겟 스켈레톤에 Rig 애셋 생성

타겟 스켈레톤을 더블클릭하여 페르소나 에디터를 열고 소스 스켈레톤에 설정했던 Rig 애셋을 동일한 방법으로 설정한다. 모든 본에 대응하는 타겟 본의 이름을 정확히 지정해야 한다. 위와 마찬가지로 작성된 릭애셋을 저장해두면 동일한 작업을 반복할 때 로드하여 사용할 수 있다

 

4. 소스 애니메이션 리타겟 및 애니메이션 생성

사용하고자 하는 소스 애니메이션 위에서 마우스 우측 > Retarget Anim Assets > Duplicate Anim Assets and Retarget


5. Select Skeleton(타겟 스켈레톤 선택)

새로 나타난 창의 왼쪽에는 소스 스켈레톤의 기본 포즈가 표시되어 있고 우측 Target 영역은 아직 비어 있는 상태이다

왼쪽 중간 쯤에 표시된 새로운 스켈레톤 이름을 선택하면 우측에 Target 영역에 해당 스켈레톤의 기본 포즈가 표시된다

소스 스켈레톤의 기본포즈와 타겟 스켈레톤의 기본포즈가 서로 일치하는지 확인한다

기본포즈가 서로 일치하지 않으면 애니메이션에도 영향을 미치게 되므로 팔을 벌린 정도나 다리를 벌린 정도, 팔의 전후 위치 등을 비교하여 소스 스켈레톤의 자세를 변경해 주면 된다


6. 소스 스켈레톤의 기본포즈 수정

언리얼 엔진에 포함된 마네킹 캐릭터의 기본포즈는 A 자형인데 mixamo 에서 임포트한 캐릭터는 T 자형 포즈를 기본으로 하고 있으므로 리타겟팅을 수행하기 전에 양 캐릭터의 기본 자세를 최대한 동일하게 설정해 주어야 애니메이션이 정상적으로 작동한다.

스켈레톤 에디터를 열고 왼쪽의 Select Bone 탭에서 수정하고자 하는 뼈를 찾아 선택한다

뷰포트의 편집툴에서 회전툴 등을 이용하여 본을 회전하여 타겟 스켈레톤의 기본포즈와 같이 설정한다

타겟 매니저 화면의 하단에 있는 [자세변경] 버튼을 누르고 [현재자세 사용] 버튼을 누른다. [자세 보이기] 버튼을 누르면 다시 원래의 기본자세가 표시되고 [자세 감추기] 버튼을 누르면 수정된 자세가 표시된다


4번 항목을 다시 수행하고 창의 하단에 있는 Select 버튼을 누르면 새로운 애니메이션이 생성된다


7. 위에서 생성된 애니메이션을 타겟 캐릭터에 설정하기

언리얼 3인칭 프로젝트에서 뷰포트 상의 마네킹을 선택하고 우측 [디테일] 섹션의 [블루프린트 편집] 버튼을 누르고 에디터의 왼쪽 [컴포턴트 추가] 섹션에서 Mesh 노드를 선택하면 우측 [디테일] 섹션의 Mesh 항목 설정에서 메시를 설정하면 마네킹 캐릭터 대신에 mixamo 등의 외부에서 임포트한 캐릭터를 설정할 수 있다

애니메이션 섹션에서는 위에서 생성한 애니메이션을 설정하고 저장한다

애니메이션 블루프린트를 생성하여 위의 애니메이션을 사용할 수도 있다


8. 플레이 버튼을 눌러 적용된 캐릭터와 애니메이션이 잘 작동하는지 확인한다


Unreal - Mixamo Bone Mapping ( Humunoid Rig)

 

 root

 None

 pelvis   Hips 
 spine_01  Spine 
 spine_02  Spine1 
 spine_03  Spine2 
 neck_01 Neck 
 head  Head
 clavicle_l  LeftShoulder
 upperarm_l  LeftArm
 lowerarm_l  LeftForeArm
 hand_l  LeftHand
 clavicle_r  RightShoulder
 upperarm_r  RightArm
 lowerarm_r  RightForeArm
 hand_r  RightHand
 thigh_l  LeftUpLeg
 calf_l  LeftLeg
 foot_l LeftFoot
 ball_l  LeftToeBase
 thigh_r  RightUpLeg
 calf_r RightLeg
 foot_r  RightFoot
 ball_r  RightToeBase
 thumb_01_l   LeftHandThumb1
 thumb_02_l   LeftHandThumb2
 thumb_03_l LeftHandThumb3
 index_01_l   LeftHandIndex1
 index_02_l   LeftHandIndex2
 index_03_l   LeftHandIndex3
 middle_01_l LeftHandMiddle1
 middle_02_l   LeftHandMiddle2
 middle_03_l   LeftHandMiddle3
 ring_01_l   LeftHandRing1
 ring_02_l LeftHandRing2
 ring_03_l   LeftHandRing3
 pinky_01_l   LeftHandPinky1
 pinky_02_l   LeftHandPinky2
 pinky_03_l   LeftHandPinky3
 thumb_01_r   RightHandThumb1
 thumb_02_r   RightHandThumb2
 thumb_03_r   RightHandThumb3
 index_01_r   RightHandIndex1
 index_02_r   RightHandIndex2
 index_03_r   RightHandIndex3
 middle_01_r   RightHandMiddle1
 middle_02_r   RightHandMiddle2
 middle_03_r   RightHandMiddle3
 ring_01_r   RightHandRing1
 ring_02_r   RightHandRing2
 ring_03_r   RightHandRing3
 pinky_01_r   RightHandPinky1
 pinky_02_r RightHandPinky2
 pinky_03_r   RightHandPinky3


Posted by cwisky