반응형

내가 대학교에 입학한 후 1학년인데 학생 연구원을 해볼 생각이 없는지 물어보셨었다.

나는 좋다고 한 후 들어갔는데

교수님이 연구를 위해 Tobii Eye Tracker를 빌려주셔서 그 기회에 이 프로젝트를 해보았다.


눈 제어해보기 전에 이것을 만들기 이전에 어떤 과정이 있었는지 말하면..

 

일단 내가 받은 아이트래커는 

이거인데 Tobii Eye Tracker 5이다. 받고난 후 어떻게 할지 검색해보다가 알게 되었다..

내가 받은건 게임용이고 연구용이 아니기에 연구용 SDK가 없다는 것을..

 

아무리 검색해도 이 기기에서는 아이트래킹 정보를 얻을 수가 없었다.

https://developer.tobii.com/product-integration/stream-engine/

 

Stream Engine - Tobii Developer Zone

The Stream Engine SDK is a low-level SDK intended for advanced users wanting to have tight control over the system resources used by Stream Engine and least amount of signal latency. To get started with the StreamEngine SDK, head over to the getting starte

developer.tobii.com

그러다가 이 것이 나와서 찾아봤는데.. 이건 일단 되는 것이다!

게임용으로 지원하는 SDK이고 이 SDK를 사용해서 아이트래킹이 지원되는 게임을 만드는 것 같다.

 

그래서 이걸 쓰기위해 보니 메일을 보내서 요청하라고 적혀있다.. 그래서 보냈더니..

한국어로 답변이 왔는데.. 아이트래커에 해킹을 시도하거나 다르게 이용을 하는 경우가 있어서 SDK를 못 준다는 말만 왔었다.. 개인적으로 사용할 것이라고 얘기해도 불가능이라고 했다.

 

그래서 그냥 포기할까 하고 한가지 생각이 들었다. 이 회사는 유명하니깐 누군가가 Github에 올려놓지 않았을까 하고..

그래서 찾아보니 진짜로 있었다. 32비트, 64비트 모두 Github에 아이트래커 프로젝트를 올리면서 같이 올려진 것들이 있다.

그래서 나는 이걸 다운받아서 시도했더니 아이트래커에 접근이 됬었던 것!

 

(근데 한가지 슬픈건.. 이 SDK가 C++과 C#만 가능한데.. 연구원에서.. 교수님과 나만 C++이 가능했던 것.. 그래서 이 연구와 관련된 프로그램을 제작하는건 모두 내가 맡아서 했었다.. 파이썬으로 실행할 수 있는 것이 github에 있던데.. 더 별로여 보여서 그냥 C++로 하기로 했다..)

 

https://developer.tobii.com/product-integration/stream-engine/tutorial_cplusplus/

 

Stream Engine Tutorial - C++ - Tobii Developer Zone

This policy applies to solutions that store or transfer Tobii eye tracking data, presence or position data, regardless of the purpose for the storing or transferring. End-users care about their data integrity and privacy. Therefore, as a vendor you must ga

developer.tobii.com

아무튼 예제 코드는 여기에 있다. C++ 프로젝트로 사용하는 방법도 나와있고..

뭐 일단 이제 VRChat과 관련된 얘기를 해보겠다!

일단 VRChat에는 OSC 라는 기능이 있다.

https://docs.vrchat.com/docs/osc-overview

 

OSC Overview

Intro to OSC OSC is a way to get different devices and applications to talk to each other. It's a favorite method of creative coders and people making weird interactive things because it's fast, networked, and very open-ended. What does this have to do wit

docs.vrchat.com

여기에 자세히 나와있는데 OSC를 이용하면 VRChat에서 아바타나 VRChat 기능과 소통하면서 여러가지 기능들을 제어할 수가 있는 것 같다.

예전에는 이걸로 번역기도 만들어보긴 했는데.. 굉장히 간단하게 할수가 있었다.

 

아무튼 저 글에서 보면 

https://docs.vrchat.com/docs/osc-eye-tracking

 

OSC Eye Tracking

VRChat now offers support for receiving eye tracking data (eyelook and eyelid) via OSC. Please note: This is an advanced feature! It is NOT plug-and-play. You must create your own program to transmit this data to VRChat using OSC. Hardware manufacturers ma

docs.vrchat.com

 

 

아이트래킹을 할 수 있는 링크가 설명이 되어있는데 대충 이거다.

/tracking/eye/EyesClosedAmount

/tracking/eye/CenterPitchYaw
/tracking/eye/CenterPitchYawDist
/tracking/eye/CenterVec
/tracking/eye/CenterVecFull
/tracking/eye/LeftRightPitchYaw
/tracking/eye/LeftRightVec

(참고로 여기로 9000 포트와 함께 데이터를 보내면 OSC가 작동한다.)

 

일단 Stream Engine을 살펴보니 잘은 모르겠지만 내가 보고 있는 시선의 위치만 가져올 수 있는 것 같다. 뭐.. 게임 용이라 그런 것 같은데.. 아니면 내가 제대로 안 찾아본 것일 수도 있고..

 

아무튼 내가 보고 있는 위치만 가져올 수 있으니깐.. 사용할 수 있는 OSC는 "/tracking/eye/CenterPitchYaw" 이거다. 이거는 문서에 잘 설명 되어있다.

그리고 눈을 감았는지에 대한건 "/tracking/eye/EyesClosedAmount" 이걸 사용하면 될 것 같다.

 

아무튼 이걸 알았으니 이제 예제 코드를 실행해볼껀데, 그냥 실행하면 이런 오류가 난다. 아마도 내꺼가 Github에서 가져와서 버전이 낮은 것 때문에 그런 것 같은데..

// Connect to the first tracker found
tobii_device_t* device = NULL;
result = tobii_device_create(api, url, TOBII_FIELD_OF_USE_STORE_OR_TRANSFER_FALSE, &device);
assert(result == TOBII_ERROR_NO_ERROR);

여기 코드에서 TOBII_FIELD_OF_USE_STORE_OR_TRANSFER_FALSE 이게 오류가 난다.

 

그냥 해결 방법은 그냥 저거 빼고 

사진에 나온거 2개 중에 아무거나 넣으면 된다.

그리고

이런 오류 나면.. 뭐 다르게 해결하는 방법이 있지만 빠르게 테스트 하기 위해 맨 상단에 이걸 넣는다.

#define _CRT_SECURE_NO_WARNINGS

아무튼 이러면 오류가 사라진다.

 

이제 실행해보면 Tobii Eye Tracker이 정상적으로 연결 되어있는 경우 작동하는데 해보면 내가 보고 있는 시선이 x, y로 나올꺼다. 그런데 내가 눈을 감거나 감지할 수 없는 영역을 쳐다보면 아무것도 출력이 안되는데.. 이걸 알 수 있다는 것은 눈을 깜박이는 것을 알 수 있다는 것이다! 확실히 알기 위해 코드를 수정했다!

void gaze_point_callback(tobii_gaze_point_t const* gaze_point, void* /* user_data */)
{
    // Check that the data is valid before using it
    if (gaze_point->validity == TOBII_VALIDITY_VALID)
        printf("Gaze point: %f, %f\n",
            gaze_point->position_xy[0],
            gaze_point->position_xy[1]);
    else // 추가
        printf("Gaze Error\n"); // 추가
}

// 추가 라고 되어있는 것이 내가 추가한건데 그냥 인식 안되면 출력하라는거다..

이렇게 하면 정상적으로 눈을 깜으면 Gaze Error이 표시되고 감지 영역에서 눈을 움직이면 쳐다보는 곳이 잘 출력된다..!

 

그리고 한가지 알아둘 것이 있는데 출력되고 있는 것을 보면

Gaze point: 0.695525, 0.411705
Gaze point: 0.634280, 0.405759
Gaze point: 0.541244, 0.393794
Gaze point: 0.465725, 0.383487
Gaze point: 0.423483, 0.384978
Gaze point: 0.402698, 0.381798
Gaze point: 0.368750, 0.379669
Gaze point: 1.126799, 0.609565
Gaze point: 1.110942, 0.619803
Gaze point: 1.121417, 0.617664
Gaze point: 1.146164, 0.605248
Gaze point: 1.153657, 0.606785
Gaze point: 1.154399, 0.599591
Gaze point: 1.155324, 0.597877

 

 

이런게 있는데.. 이 숫자에 대한 것이다.

이 숫자는 그냥 x와 y의 시선 값인데 0부터 1 사이 값들은 모니터 안에 있는 것이고

1 이상들은 모니터 밖을 쳐다보고 있다는 것이다.

또한 모니터가 1920 * 1080 이면 각각에 x와 y 값을 곱해보면 내가 보고 있던 픽셀을 알수도 있다는 것이다.

 

이번에는 VRChat OSC에 정보를 보내는 방법에 대한 얘기인데

Python이라면 그냥 python-osc 이거 사용해서 로컬 9000 포트로 보내면 끝난다.

근데 C++이기에 OSC 라이브러리를 사용해야한다.

 

일단 난 그냥 간단하게 이걸 사용했다.

https://github.com/CINPLA/oscpack

 

GitHub - CINPLA/oscpack: Automatically exported from code.google.com/p/oscpack

Automatically exported from code.google.com/p/oscpack - CINPLA/oscpack

github.com

 

아무튼 바로 코드!

#include "tobii/tobii.h"
#include "tobii/tobii_streams.h"
#include <iostream>
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <conio.h>
#include <Windows.h>
#include <typeinfo>
#include "osc/OscOutboundPacketStream.h"
#include "ip/UdpSocket.h"
#include <opencv2/opencv.hpp>

#pragma comment(lib, "Ws2_32.lib")

#define CanvasW 1920 / 4
#define CanvasH 1080 / 4

const char* ip = "127.0.0.1";
const int port = 9000;

float last_sent_value = -1.0f; // 이전에 보낸 값을 저장할 변수
bool test_mode = true; // 테스트 모드 설정

cv::Point2f current_point(0.0f, 0.0f); // 현재 점의 위치
cv::Point2f target_point(0.0f, 0.0f); // 목표 점의 위치

const float gaze_x_min = -25.0f;
const float gaze_x_max = 25.0f;
const float gaze_y_min = -15.0f;
const float gaze_y_max = 20.0f;

cv::Mat canvas(CanvasH, CanvasW, CV_8UC3, cv::Scalar(255, 255, 255)); // canvas 선언

void send_message(UdpTransmitSocket& transmitSocket, const char* address, float value) {
    if (value == last_sent_value) return; // 이전에 보낸 값과 같다면 보내지 않음

    char buffer[1024];
    osc::OutboundPacketStream p(buffer, 1024);

    p << osc::BeginMessage(address) << value << osc::EndMessage;
    transmitSocket.Send(p.Data(), p.Size());

    last_sent_value = value; // 보낸 값을 저장
}

void send_gaze_message(UdpTransmitSocket& transmitSocket, const char* address, float x, float y) {
    char buffer[1024];
    osc::OutboundPacketStream p(buffer, 1024);

    p << osc::BeginMessage(address) << x << y << osc::EndMessage;
    transmitSocket.Send(p.Data(), p.Size());
}

void gaze_point_callback(tobii_gaze_point_t const* gaze_point, void* user_data)
{
    UdpTransmitSocket* transmitSocket = static_cast<UdpTransmitSocket*>(user_data);
    if (gaze_point->validity == TOBII_VALIDITY_VALID) {
        printf("Gaze point: %f, %f\n",
            gaze_point->position_xy[0],
            gaze_point->position_xy[1]);

        float x = gaze_point->position_xy[0] * (gaze_x_max - gaze_x_min) + gaze_x_min;
        float y = gaze_point->position_xy[1] * (gaze_y_max - gaze_y_min) + gaze_y_min;

        send_gaze_message(*transmitSocket, "/tracking/eye/CenterPitchYaw", y, x);

        send_message(*transmitSocket, "/tracking/eye/EyesClosedAmount", 0.0f);

        if (test_mode) {
            target_point = cv::Point2f(gaze_point->position_xy[0] * CanvasW, gaze_point->position_xy[1] * CanvasH);
            current_point = target_point;
            cv::circle(canvas, current_point, 3, cv::Scalar(255, 0, 0), -1); // 파란색으로 변경
        }
    }
    else {
        std::cout << "Invalid" << std::endl;
        send_message(*transmitSocket, "/tracking/eye/EyesClosedAmount", 1.0f);

        if (test_mode) {
            current_point = target_point;
            cv::circle(canvas, current_point, 3, cv::Scalar(0, 0, 255), -1); // 빨간색으로 변경
        }
    }
}

void url_receiver(char const* url, void* user_data)
{
    char* buffer = (char*)user_data;
    if (*buffer != '\0') return; // only keep first value

    if (strlen(url) < 256)
        strcpy_s(buffer, 256, url); // strcpy_s로 변경
}

int main()
{
    // Create API
    tobii_api_t* api = NULL;
    tobii_error_t result = tobii_api_create(&api, NULL, NULL);
    assert(result == TOBII_ERROR_NO_ERROR);

    // Enumerate devices to find connected eye trackers, keep the first
    char url[256] = { 0 };
    result = tobii_enumerate_local_device_urls(api, url_receiver, url);
    assert(result == TOBII_ERROR_NO_ERROR);
    if (*url == '\0')
    {
        printf("Error: No device found\n");
        //return 1;
    }

    // Connect to the first tracker found
    tobii_device_t* device = NULL;
    result = tobii_device_create(api, url, TOBII_FIELD_OF_USE_ANALYTICAL, &device);
    std::cout << typeid(result).name() << std::endl;
    assert(result == TOBII_ERROR_NO_ERROR);

    // Subscribe to gaze data
    UdpTransmitSocket transmitSocket(IpEndpointName(ip, port));
    result = tobii_gaze_point_subscribe(device, gaze_point_callback, &transmitSocket);
    assert(result == TOBII_ERROR_NO_ERROR);

    // Initialize socket and server address

    while (true) {
        // Optionally block this thread until data is available. Especially useful if running in a separate thread.
        result = tobii_wait_for_callbacks(1, &device);
        assert(result == TOBII_ERROR_NO_ERROR || result == TOBII_ERROR_TIMED_OUT);

        // Process callbacks on this thread if data is available
        result = tobii_device_process_callbacks(device);
        assert(result == TOBII_ERROR_NO_ERROR);

        if (test_mode) {
            cv::imshow("Gaze Point", canvas);
            cv::waitKey(1);
            canvas.setTo(cv::Scalar(255, 255, 255));
        }

        if (_kbhit()) {
            int ch = _getch();
            if (ch == 27) // ESC Key
                break;
        }
    }

    // Cleanup
    result = tobii_gaze_point_unsubscribe(device);
    assert(result == TOBII_ERROR_NO_ERROR);
    result = tobii_device_destroy(device);
    assert(result == TOBII_ERROR_NO_ERROR);
    result = tobii_api_destroy(api);
    assert(result == TOBII_ERROR_NO_ERROR);
    return 0;
}

 

일단 이건 내가 그냥 간단하게 테스트만 해볼려고 만든 코드이다.

그래서 코드가 더러운 것도 있고 굳이 필요 없는 기능이 있기도 하다.

뭐 그래도 대충 이 코드를 실행하면 정상적으로 눈 제어가 가능한 아바타에서는 작동한다!!

 

그리고 코드에 보면 캔버스 설정한게 있는데 그냥 그건 OpenCV로 어디를 보고 있는지 표시하기 위해 놔둔 것이다. test_mode 끄면 나오지 않는다.

 

const float gaze_x_min = -25.0f;
const float gaze_x_max = 25.0f;
const float gaze_y_min = -15.0f;
const float gaze_y_max = 20.0f;

그리고 이런 코드가 있는데.. 이 코드는 VRChat OSC로 데이터를 보낼 때 데이터 범위이다.

각 아바타 마다 데이터의 범위가 정해져있는 것 같다. 일단 내가 사용하는 아바타는 일일이 값을 보내서 테스트 해본 결과 저정도 인 것 같다.

 

아무튼 이렇게 하면 눈 제어와 눈 깜박이는 것을 모두 아이트래커로 가능하다! 심지어 놀라운건 나는 아이트래킹은 VR 유저만 가능한 줄 알았는데 데스크탑에서도 가능하다는 것..

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

이게 테스트 영상!

 

아무튼 끝!

반응형
반응형

오류	C2857	/Ycpch.h 명령줄 옵션과 함께 지정한 '#include' 문이 소스 파일에 없습니다.

 

오늘 프로그래밍 도중에 이런 오류가 떴다..

일단 나의 경우에는 파일 분할 때문에 정리할려고 include, source 폴더를 만들고 거기에 있는 것을 가져오는 방식으로 구현할려고 했다.

그런데 이런 문제가 발생해서 검색해보니.. 모든 사람들이 그냥 미리 컴파일 된 헤더를 비활성화 하라고 한다..

나는 미리 컴파일 된 헤더를 활성화 시킬려고 하는건데..

 

아무튼 원인은 잘 모르겠지만

#include "pch.h" <- *.h 파일
#include "../include/pch.h" <- *.cpp 파일

각 파일에는 위에 적힌 대로 있다. 

그런데 문제는

이 미리 컴파일 된 헤더 파일 이름이 pch.h로만 되어있는데 저게 그대로 include가 되어있는지를 감지하고 안되어있으면 저런 오류를 뱉어내는 것 같다.

그래서 미리 컴파일 된 헤더 파일 이름을 ../include/pch.h로 해봐도 .h 파일 쪽에 문제가 있을테니.. 할 수 있는 방법은 없었다.

 

그러다가 생각난게 다른 라이브러리를 연결할 때 C/C++일반에서 추가 포함 디렉터리를 사용해야한다는게 생각나서 include 폴더 경로를 C/C++일반추가 포함 디렉터리에 추가를 한 후에 이렇게 하면 어차피 ../include 같은건 자동으로 되니깐 .cpp 파일에서 모두 수정하고 실행하니 해결되었다!

$(ProjectDir)\include

위에 이건 프로젝트 경로 안의 include 폴더를 지정하도록 하는 그런거다.

 

그리고 이런 오류가 뜨는 경우도 있는데 그냥 이건 pch.cpp 파일이나 stdafx.cpp 파일의 속성에서 

"사용(/Yu)"로 되어있을 텐데 "만들기(/Yc)"로 안해서 이다. "만들기(/Yc)"로 변경해주면 끝

 

그리고 $(ProjectDir) 같은건 여기에 보면 잘 설명 되어있다.

https://learn.microsoft.com/en-us/cpp/build/reference/common-macros-for-build-commands-and-properties?view=msvc-170

 

Common macros for MSBuild commands and properties

Learn more about: Common macros for MSBuild commands and properties

learn.microsoft.com

 

반응형
반응형

오늘 WinRT로 프로젝트를 제작하면서 OpenCV를 사용해야해서 OpenCV 코드를 작성하고 컴파일을 했는데 컴파일은 분명 잘되는데 프로그램을 실행하면서 처음에는 잘 되다가 다시 실행하면 아래 사진 처럼 오류가 난다.. 도무지 모르겠는데.. 그러다가 12시간 동안 뭔 문제인지 찾다가 알아냈다. (참고로 진짜 12시간 걸렸다.)

Windows 스토어 응용 프로그램 'APP_ID'을(를) 활성화할 수 없습니다. 활성화 요청이 실패했습니다(오류: '앱이 시작되지 않았습니다').

문제 해결에 필요한 정보는 도움말을 참조하십시오.

 

일단 이 원인.. 잘은 모르겠지만 프로그램을 실행하는 도중에 Console 창에서 오류가 나는걸 못 띄우니 여기에서라도 실행할 때 오류를 띄우는 것 같다. 그렇다 보니 원인도 모르겠고.. 

디버깅을 할려고 해도 처음부터 저게 떠버려서 디버깅도 할 수가 없었다..

 

아무튼 방법은 찾았는데 그냥 OpenCV dll 파일이 없어서였다.. (왜 이것 때문에.. 12시간을 고생한거지;;)

 

OpenCV VCPKG WinRT용 설치

일단 한가지 알아낸 것이 있는데 WinRT에서 OpenCV를 돌릴려면 WinRT 용으로 바꿔야하는 것 같다.

WinRT는 UWP로 뭐 돌아가는건데 VCPKG에서 UWP로 설치하는 버전이 있다.

설치는 아래처럼 하면 된다.

vcpkg install opencv4:x64-uwp

이런식으로 설치하면 uwp 버전으로 설치가 된다.

 

오류 해결

앞에서 말했 듯이 그냥 이건 dll 파일이 없어서 생긴 오류이다. 실행에 필요한 dll 파일을 아래 경로에 붙여넣기 하면 오류가 해결된다.

<ProjectName>\x64\Release\<ProjectName>\AppX

그런데 한가지 문제가 있다.. 빌드 할 때마다 dll 파일이 사라져서 자동으로 복붙이 되게 해야하는데

이럴 때는 방법이 있다. 

 

한가지는 그냥 Visual Studio 프로젝트 파일에서 새 필터로 dll 파일 추가해서 하는 방법이 있는데 이건 비추하고..

다른 방법으로는 자동으로 복붙되게 하는 것이다.

 

일단 프로젝트 파일을 오른쪽 클릭하고 속성을 열어서 나오는 창에서

빌드 이벤트의 빌드 후 이벤트로 들어간다.

들어가게 되면 명령줄이 있는데 여기에 코드만 넣으면 된다.

 

일단 빌드 후 이벤트는 말그대로 빌드가 된 후에 자동으로 명령줄에 있는걸 실행하게 되는 것이다.

 

코드는 이걸 넣으면 된다!

xcopy /y "$(OutDir)\*.dll" "$(OutDir)\AppX"

일단 내가 대충 만들어서 해봤을 때 되는 코드이니.. 이게 잘 되는 코드인지는 모르겠다만.. 아무튼 난 저걸 명령줄에 넣어서 했더니 자동으로 복붙이 되었고 해결이 되었다.

 

코드는 대충 AppX에는 dll이 없지만 다행이도 출력 폴더에는 dll 파일이 있다. 그래서 모든 dll 파일을 가지고 AppX 폴더로 붙여 넣는 코드이다.

 

아무튼 이걸로 해결!

반응형
반응형

학생 연구원에서 연구과제로 Muse 2를 받게 되었다.

그런데 기존에 다른 분이 조사하신건 Python으로 Muse LSL를 설치해서 하는 방법인데, 이 방법은 Python으로만 할 수 있는걸로 알고 있는데 프로젝트가 C++로 해야하는데 이 언어도 지원 안하고 심지어 추가적인 BlueMuse인가 하는 것도 설치해야한다.

 

그렇기 때문에 나는 BrainFlow를 알게되서 이 방법을 사용하기로 했는데 굉장히 어려웠다.. 4시간을 이러고 있었으니.. 내일 수업인데도 새벽 3시까지..

 

(혹시나 몰라서 말하는거지만 아래 글에 있는 경로 그대로 붙여넣으면 안된다. pkdpm은 그냥 내 컴퓨터 이름이기 때문에 자신의 컴퓨터 이름으로 바꿔야한다.)

 

Muse 2, Muse S 블루투스에 대해서

일단 Muse의 경우 블루투스로 연결하게 된다. 여기에서 문제는 아마 이게 BLE 같은데.. 저전력 블루투스.. 그렇다 보니 연결도 BlueMuse 같은 방법으로 연결하는 듯 하다.. 

그런데 다행이도 BrainFlow에선 무언가 설치 안하고 바로 동작할 수 있게 되어있는데 기존에는 BLED112라는 제품을 사용해야했다. https://brainflow.org/2021-06-22-muse-bled/ 이게 처음 Muse 지원했을 때 글 같은데 BLED112라는게 필수적으로 필요했다. 

그런데 https://brainflow.org/2021-11-01-new-release/ 지원 4개월 후 쯤에 BLED112를 사용하지 않고도 가능하게 바뀌었다라는 소식이 있었다.

이거를 할려면 글에 나온 것 처럼 빌드 할 때 --ble를 추가하던지, CMake에서 DBUILD_BLE를 ON으로 해야하는 것 같다.

(참고로 일반적으로 블루투스 연결할려고 하면 전혀 목록에 뜨지 않는게 정상이다. 모바일은 뜨긴 하지만..)

 

아무튼 일단 Python으로 설치해보기! - (전 안되네요..)

일단 공식적으로 파이썬으로 빠르게 설치하는 방법이 있는데.. 이상하게도 기본 방법은 설치가 되는데 --ble 라는 파라미터를 추가하면 빌드가 이상하게 오류가 뜬다.. 왜인지는 잘 모르겠지만..

일단 관련 글은 여기에 있다.

https://brainflow.readthedocs.io/en/stable/BuildBrainFlow.html#compilation-of-core-module-and-c-binding

 

Installation Instructions — BrainFlow documentation

R R binding is based on reticulate package and calls Python , so you need to install Python binding first, make sure that reticulate uses correct virtual environment, after that you will be able to build R package from command line or using R Studio, insta

brainflow.readthedocs.io

git clone https://github.com/brainflow-dev/brainflow.git
python -m pip install cmake
cd brainflow/tools
python build.py --help

일단 위에 명령어 처럼 친다. 명령어는 원하는 폴더에서 치면 되는데 나는 그냥 Downloads 폴더에서 했다.

이렇게 하면 brainflow 깃허브 파일이 모두 다운로드 되고 cmake 라이브러리를 설치하고 brainflow의 tools 폴더로 이동해서 build.py의 도움 내용을 출력하는 것이다.

 

usage: build.py [-h] [--oymotion] [--no-oymotion] [--msvc-runtime {static,dynamic}] [--generator GENERATOR]
                [--arch {x64,Win32,ARM,ARM64}] [--cmake-system-version CMAKE_SYSTEM_VERSION] [--build-dir BUILD_DIR]
                [--brainflow-version BRAINFLOW_VERSION] [--cmake-install-prefix CMAKE_INSTALL_PREFIX] [--use-openmp]
                [--onnx] [--warnings-as-errors] [--debug] [--clear-build-dir] [--num-jobs NUM_JOBS] [--bluetooth]
                [--no-bluetooth] [--ble] [--no-ble] [--tests] [--no-tests]

options:
  -h, --help            show this help message and exit
  --oymotion
  --no-oymotion
  --msvc-runtime {static,dynamic}
                        how to link MSVC runtime
  --generator GENERATOR
                        generator for CMake
  --arch {x64,Win32,ARM,ARM64}
                        arch for CMake
  --cmake-system-version CMAKE_SYSTEM_VERSION
                        system version for win
  --build-dir BUILD_DIR
                        build folder
  --brainflow-version BRAINFLOW_VERSION
                        BrainFlow Version
  --cmake-install-prefix CMAKE_INSTALL_PREFIX
                        installation folder, full path
  --use-openmp
  --onnx
  --warnings-as-errors
  --debug
  --clear-build-dir
  --num-jobs NUM_JOBS   num jobs to run in parallel
  --bluetooth
  --no-bluetooth
  --ble
  --no-ble
  --tests
  --no-tests

 

일단 이게 내용인데 일단 내가 아는 것 중에 먼저 말하자면 나중에 C++에서 사용할 때 Release로 빌드 해야하는 것 같다.. 만약에 Debug로 빌드를 하고 싶으면 --debug를 추가해야하는 것 같다. 근데 또 저거 추가하면 Release가 안먹힌다...

그리고 기본적으로 --bluetooth는 켜져있다. (bluetooth는 일반 블루투스 장치를 지원하는 것 ble와는 다른 것)

그리고 --ble가 필요한건데 저전력 블루투스를 지원하게 하는 것이다. 그렇기 때문에 명령어는 아래 처럼 치면 된다.

python build.py --ble

그런데 위에 소제목에서 말한 것 처럼 나는 이게 이상하게도 안된다.. (더 신기한건 --ble를 제거하면 되긴 한다)

실행하면 아래처럼 오류가 뜬다. (글 쓰면서 실행하는데.. 역시 C++ 빌드는 너무 오래걸리는..)

  Brainflow.vcxproj -> C:\Users\pkdpm\Downloads\brainflow\build\Release\Brainflow.lib
Traceback (most recent call last):
  File "C:\Users\pkdpm\Downloads\brainflow\tools\build.py", line 306, in <module>
    main()
  File "C:\Users\pkdpm\Downloads\brainflow\tools\build.py", line 302, in main
    build(args)
  File "C:\Users\pkdpm\Downloads\brainflow\tools\build.py", line 287, in build
    run_command(cmd_build, cwd=args.build_dir)
  File "C:\Users\pkdpm\Downloads\brainflow\tools\build.py", line 22, in run_command
    raise ValueError('Process finished with error code %d' % p.returncode)
ValueError: Process finished with error code 1

 

이런 오류가 뜨게 된다.. 아무리 해도 지우고 다시 해보고 다른 것도 켜보고 하는데도 안되었다.. (이것 때메 1시간을 날린..)

그래서 결국 안되서 그냥 수동으로 빌드하기로 했다.. 

 

2024-09-15 변경 - 해결 방법

지금 이건 9월 15일인 오늘 추가로 작성하는 것이다.

나는 이 문제가 다시 무엇인지 볼려고 확인을 해보니.. 문제를 알아냈다. 

간단하게 말한다면 컴파일을 하는데 UTF-8로 컴파일이 되지 않고 다른 인코더로 컴파일이 되면서 오류가 발생한 것 같다.

이 문제를 해결하는 제일 간단한 방법은 윈도우 검색 창에 "국가 또는 지역"을 검색하고 창을 열어서 관리자 옵션에 들어가면

이런식으로 "시스템 로캘 변경"이라고 뜬다. 저걸 눌른 후 

아래 보이는 "Beta: 세계 언어 지원을 위해 Unicode UTF-8 사용"를 활성화 한다.

이렇게 한 후에 다시시작하고 컴파일을 하면 정상적으로 된다.

아마도 한국어 윈도우나 일본어 윈도우 이런건 아마도 EUC-KR EUC-JP 같은 인코더로 되어있어서 컴파일할 때 저걸로 컴파일 해서 오류 나는 것 같은데 이건 윈도우에서 기본으로 쓰는 인코더를 UTF-8로 바꿔서 해결할 수 있는 것 같다.

 

일단 나는 이 문제를 발견해서 빌드할 때 처음부터 UTF-8로 빌드되도록 소스코드를 수정해서 풀 리퀘스트를 했다.

그래서 지금은 수정이 된 상태이다.

https://github.com/brainflow-dev/brainflow/pull/745

 

Fix UTF-8 encoding problem by cheongpark · Pull Request #745 · brainflow-dev/brainflow

Traceback (most recent call last): File "C:\Users\cheongpark\brainflow\tools\build.py", line 309, in <module> main() File "C:\Users\cheongpark\brainflow\tools\build.py", l...

github.com

그래서 파일을 다운받고 그냥

python build.py --ble

이렇게 빌드해도 오류 없이 정상적으로 컴파일이 된다..!

 

그리고 컴파일 되면 "installed"라는 폴더가 생기는데 여기에 inc 폴더와 lib 폴더가 생긴다. 이거를 가져다가 이 글 아래에 설명되어있는 대로 하면 정상적으로 작동한다! (도데체 왜 저 문제를 이제야 찾은걸까..)

 

추가적으로 Muse에 대한 내용이 잘 못 기재되어있다.

구글에 검색해보면 Muse 기기의 EEG 이름이 TP9, AF7, AF8, TP10 로 되어있는데 여기에서는 TP9, Fp1, Fp2, TP10 이걸로 설정 되어있다. 그래서 결국에는 이 정보를 어디에서 가져오는지 찾아내고 정확한 정보를 기재해서 이것도 풀 리퀘스트를 했다. (심지어 Muse 관련 코드를 열어봐도 Fp1, Fp2는 없고 AF7, AF8만 있다.. 엄..) (처음으로 남의 프로젝트에 풀 리퀘스트를 해보았어서.. 심장이 엄청 뛰어댔었다.. 내가 수정해도 되는게 맞을까 하고..)
https://github.com/brainflow-dev/brainflow/pull/744

 

Fix EEG names for Muse 2, Muse S, Muse 2016 by cheongpark · Pull Request #744 · brainflow-dev/brainflow

Description of Changes I noticed that the eeg_names for the Muse 2 I am using were incorrect. Upon checking, it seems that the names have changed. Original: TP9, Fp1, Fp2, TP10 Updated: TP9, AF7, A...

github.com

 

아무튼 오늘 날짜 기준으로 최신버전이 5.13.3인데

pip install brainflow

를 해도 최신버전으로 수정이 안되어있다.

이유는 간단하다. 그냥 새로운 버전을 개발자가 안올렸기 때문에..

 

뭐.. 5.13.4 쯤에는 올라와 있을테니 그 때 까지 기달려야 할 것 같다..

 

아무튼 이 아래 부터는 전에 쓴 내용이 어이진다.

 

CMake로 BrainFlow 수동 빌드하기

일단 아쉽게도 나는 CMake를 예전에 OpenCV 빌드하다가 빡쳐서 포기했던 녀석이라.. 왠만하면 쓰기 싫었다.. (지금은 VCPKG 쓰는..)

그렇지만 일단 이건 연구 과제이기 때문에 해야했다.

마찬가지로 아까 위에 올린 링크에 같이 설치 방법이 있다. (저거 링크 들어가보면 In VS installer make sure you selected “Visual C++ ATL support” 저거를 VS 인스톨러에서 설치하라는건데 2022 버전에선 사라진 것 같다. 없기도 하고 설치 안해도 문제가 없는 것 같다.)

https://brainflow.readthedocs.io/en/stable/BuildBrainFlow.html#compilation-of-core-module-and-c-binding

 

Installation Instructions — BrainFlow documentation

R R binding is based on reticulate package and calls Python , so you need to install Python binding first, make sure that reticulate uses correct virtual environment, after that you will be able to build R package from command line or using R Studio, insta

brainflow.readthedocs.io

 

만약에 전에 위 글 처럼 파이썬으로 빌드 해보셨다면 지울게 있습니다!

혹시라도 위에 글 처럼 빌드를 한번 해보았다면 지울게 있다.

compiled, build 폴더이다. 이건 brainflow 폴더에서 모두 지우고 실행해야한다. 아니면 그냥 brainflow 폴더를 지우고 다시 git clone 하는 것도 방법..

 

CMake 설치 & 설정!

https://cmake.org/download/ 여기에서 나는 Windows 이기 때문에 Windows x64 Installer 이걸로 선택해서 설치했다.

설치하는건 별로 어려운게 아니여서 패스..

 

이제 CMake로 구성 설정하고 프로젝트를 만들어야 한다. 

CMake를 설치하면 실행이 가능한데 실행을 한다!

이렇게 뜨는데 여기에서 Where is the source code에는 Browse Source... 버튼을 눌러 brainflow 위치로 잡는다.

나 같은 경우는 다운로드 폴더에 brainflow를 넣었기 때문에 이걸로 지정했다.

C:\Users/pkdpm/Downloads/brainflow

그러면 그 후에는 Where to build the binaries 폴더를 선택하는건데 이건 빌드 폴더를 만드는거다.

Browse Build...을 눌러서 폴더를 지정한다. 하지만 이건 일단 위에 있는 경로로 설정을 하고 뒤에 build만 붙이면 된다.

이렇게

C:/Users/pkdpm/Downloads/brainflow/build

이렇게 하면 아래처럼 된다.

이제 여기에서 프로젝트를 만들어야 하는데

아래에 있는 Configure 버튼을 눌른다.

혹시 만약에 아래 사진 처럼 경고 같은거 뜨면 Yes 눌르면 된다. build 폴더 지정했으면서 build 폴더를 안만들었으니 만들어주겠다는 표시이다.

 

암튼 Configure 눌르게 되면 이렇게 뜨는데

여기에서 맨 위에 있는 Visual Studio 17 2022는 그냥 자신이 설치한 Visual Studio 버전이다. BrainFlow에선 2019를 권장하고 2017만 말하고 있는데 테스트 해보니 2022도 상관없는 것 같다. 그냥 바로 Finish 눌른다.

 

그런 후에는 아무것도 없던 곳에 이런 화면처럼 바뀐다.

이제 여기에서 어떤걸 빌드할지 선택할 수 있는데 여기에서 Muse 2나 Muse S가 지원될 수 있는 BUILD_BLE를 선택한다.

그리고 여기에서 좀 고생한건데.. 나중에 컴파일 하다 보면 이상하게 INSTALL 프로젝트를 빌드하면 setlocal 이러면서 오류가 뜬다.. 대충 나한테 권한 없는 곳에 폴더 두라 했다면서 화내는 오류인 것 같다.

그렇기 때문에 미리 방지하기 위해 CMAKE_INSTALL_PREFIX 부분을 수정한다. 저게 이제 INSTALL 프로젝트를 빌드하면 빌드된걸 모으는건데 어디로 모을껀지인거다.

폴더 위치는 아무데나 잡아도 되는데 관리자 권한 필요없는 폴더로 지정한다. 나는 그냥 brainflow 안에 install 폴더 안에 넣기로 했다.

내가 말한거 키면 이렇게 아래처럼 된다.

이제 아래쪽에 Generate 버튼 눌르면 바로 프로젝트가 만들어지게 된다.

다 만들어지고 나면 아래 사진처럼 원래는 Open Project 버튼이 비활성화였는데 활성화로 바뀌게 된다.

그러면 빌드 하기위해 바로 Open Project를 눌른다! 

 

빌드하기

Open Project를 눌르면 바로 Visual Studio가 켜지는데 만약에 안켜지만 직접 build 폴더 가서 키는걸로..

암튼 킨 후에 위에 보면

이렇게 생긴게 있다. 어떤걸로 빌드할지인데 아까전에 CMake 설정할 때 Debug는 따로 설정안했기 때문에 Release로 바꾼다. 그냥 눌러서 Release로 바꾸면 된다.

이렇게

그 후에는 이제 빌드를 해야하는데 ALL_BUILD를 눌러서 전부 빌드하면 좋겠지만 이상하게도 아까 추가했던 BLE에 대한건 ALL_BUILD에 추가가 안되어있는 것 같다. (BLE 프로젝트 빌드 안하고 하면 뭐 없다면서 오류남)

그래서 먼저 BLE 프로젝트 부터 빌드 해야하는데 그냥 simpleble 프로젝트를 오른쪽 클릭해서 빌드 버튼 눌르면 된다.

조금 시간이 걸리긴 하는데.. 하다보면 뜰 수도 있고 안뜰 수도 있는데 나 처럼 오류가 생길 수 있다.

오류	C2220	다음 경고는 오류로 처리됩니다.	simpleble	chrono.h

사진 아래 경고는 딱히 상관없다. 그냥 인코더 오류인 것 같은데.. (아까는 안뜨던데..)

어쨋든 chrono.h가 뭔 문제인지 경고가 뜨는거다. simpleble 프로젝트를 보면 chrono.hfmt/chrono.h에 있다고 분명 정의가 되어있고 아무 문제가 없는 것 같은데도 저런 오류가 뜬다. 근데 저건 경고인데 오류로 처리한거라 그냥 무시하도록 바꾸면 된다.

simpleble 프로젝트를 오른쪽 클릭하고 속성에 들어간다.

그럼 대충 이런식으로 뜰텐데

여기에서 왼쪽 리스트에서 C/C++ 안에 일반에 보면 사진처럼 경고를 오류로 처리는 항목이 있다.

이거 떄문에 아까 오류가 뜬건데 그냥 아니요(/WX-)로 바꾸면 된다.

그 후에 확인을 눌러서 저장하고 다시 simpleble 프로젝트 오른쪽 클릭해서 빌드 눌러서 빌드한다.

그러면 정상적으로 빌드가 되게 된다.

이런식으로 아래에 출력창에 뜬다.

이제 필요한건 다 빌드 했으니 ALL_BUILD를 빌드하면 된다.

똑같이 그냥 ALL_BUILD 프로젝트를 오른쪽 클릭하고 빌드 버튼 눌러서 빌드 하면 된다.

이건 좀더 많이 오래걸린다.. 전부 빌드해야해서..

다 하면 이렇게 3분 동안 빌드한게 뜬다..

 

이제 마지막으로 이걸 한 폴더안에 전부 모을 수 있게 프로젝트를 빌드해야하는데 바로 INSTALL 프로젝트이다.

이것도 그냥 똑같이 INSTALL 프로젝트를 오른쪽 클릭해서 빌드 버튼 눌르고 빌드하면 된다.

(만약에 혹시 setlocal인가 하는 오류 뜨면 위에 못보고 지나친 글 보면 된다.)

이렇게 하면 위에서 말한 대로 

C:/Users/pkdpm/Downloads/brainflow/install

사전에 CMake에서 설정한 곳에 빌드된게 생긴다. inclib 폴더이다. 어떤 역할인지는 대충 폴더 보면 알 수 있고.. 암튼 이거를 사용할 수 있게 어디 C 안에 두면 된다.

나 같은 경우는 전에 VCPKG 설치하면서 만들어 두었던 dev 폴더에 넣기로 했다.

C:/dev/brainflow

 

나는 이 폴더에 inclib 폴더를 넣었다.

 

예제 프로젝트 만들어서 실행해보기

이제 잘 되는지 테스트 해보기 위해 프로젝트를 만들어서 실행해봐야 한다.

일단 Visual Studio를 실행한다.

새 프로젝트 만들기 해서 빈 프로젝트를 하나 만든다.

소스 파일 오른쪽 클릭해서 새 파일로 main.cpp 파일 만들고 그 안에 아래 링크에 있는걸 복사해서 붙인다.

https://github.com/brainflow-dev/brainflow/blob/master/cpp_package/examples/get_data/src/get_data_muse.cpp

 

brainflow/cpp_package/examples/get_data/src/get_data_muse.cpp at master · brainflow-dev/brainflow

BrainFlow is a library intended to obtain, parse and analyze EEG, EMG, ECG and other kinds of data from biosensors - brainflow-dev/brainflow

github.com

그냥 어쩌다가 예제 찾다보니 찾은건데 저거 하면 CSV로 레코딩된거 10초간 기록되는 것 같다.

일단 코드에서 수정할 것이 있는데 만약에 자신꺼가 Muse S라면 상관없는데 Muse 2면 

int board_id = (int)BoardIds::MUSE_S_BOARD;

23번째 줄에 있는 이 코드를 아래처럼 바꾼다

int board_id = (int)BoardIds::MUSE_2_BOARD;

그런 후에 실행하면 당연하게도 안된다. 

일단 Debug 모드에서 실행한 것도 있고 아까 빌드한걸 연결 안했기 때문이다.

일단 이걸 Release로 우선 바꾸고

이제 빌드한걸 연결해야하는데

프로젝트를 오른쪽 클릭해서 속성에 들어간다.

아까 본 것 처럼 이런 화면 뜨는데 

먼저 C/C++일반에 들어간다.

그러면

추가 포함 디렉토리가 뜨는데 여기에 아까전에 빌드한거 뒀던 곳의 inc 경로를 붙여넣으면 된다. 나 같은 경우엔 devbrainflow에 넣었기 때문에 여기 안에 있는 inc 폴더의 경로를 넣으면 된다.

이런식으로 그 후에는 lib 퐇더를 연결시켜야 하는데

링커일반추가 라이브러리 디렉터리 부분에 lib 폴더를 넣으면 된다.

이런식으로

 

이제 어떤 lib 파일을 쓰는건지 넣는 것 같은데 암튼 이것도 해야한다.

위 사진 처럼 링커입력추가 종속성 부분에 또 뭔가를 넣어야 한다.

이건 아래꺼를 그냥 넣으면 된다. 복사해서 넣어도 되고 파일 이름 찾아서 넣어도 되고

Brainflow.lib
BoardController.lib
DataHandler.lib
MLModule.lib

넣을 때는 저거 오른쪽에 화살표 같은거 눌러서 편집 버튼 눌러서 맨 위에 빈칸에 넣으면 된다.

그 후에 아래에 확인 버튼을 눌르면 설정이 완료된다.

 

이제 모두 파일을 연결했다!

오류	LNK2038	'RuntimeLibrary'에 대해 불일치가 검색되었습니다. 'MT_StaticRelease' 값이 'MD_DynamicRelease'(main.obj에 위치) 값과 일치하지 않습니다.

그런데 또 실행해보면 이런 문제가 발생한다.

 

대충 그냥 런타임 라이브러리가 이상하게 설정되어있다는 건데 뭐.. 설정하면 된다..

다시 프로젝트 속성에 가서

C/C++코드 생성에서 런타임 라이브러리를 수정하면 된다. 보통은 다중 스레드 DLL(/MD)로 되어있을 텐데 이걸 다중 스레드(/MT)로 바꿔주면 된다. 아래처럼

(뭐.. Debug 모드에선 MTd로 설정해야 하긴 하는데 어차피 Debug 껀 빌드 안했으니 뭐..)

 

(참고로 이후에 프로젝트 진행하면서 MD로 빌드 해야해서 해보니 CMake로 Open Project 했을 때 열리는 것에서 오른쪽 ALL_BUILD 등등의 프로젝트 속성에서 C/C++의 코드 생성에서 런타임 라이브러리를 모두 MD로 바꿔주고 빌드하면 MD로 빌드된다.)

 

그 후에 확인 버튼을 눌르고 다시 실행한다! 실행하는건 Ctrl + F5로 

 

그러면 드디어 빌드가 되고 실행이 된다!

근데 이런식으로 오류가 뜨게 된다.

컴파일 한걸 실행할려는데 DLL 파일이 없어서 안되는거다. 그냥 옮기면 된다.

아까전에 빌드 했던거 옮겨놓은 폴더에 가서 lib 폴더로 들어간다.

그러면 .dll로 되어있는 파일들이 여러개 보이는데 뭐인지 확인하는건 귀찮으니깐 .dll로 되어있는 파일은 모두 복사한다.

(Ctrl 키 눌르고 여러개 선택해서 복사하면 된다.)

 

붙여 넣는 곳은 프로젝트를 오른쪽 클릭해서 파일 탐색기에서 폴더 열기(X) 버튼을 눌른 후 뜨는 폴더에 다 붙여넣으면 된다.

그렇게 되면 이렇게 붙여넣어졌을 것이다.

이러면 끝이다!

이제 Muse 제품의 전원을 키고 실행해보면? 정상적으로 연결이 된다!

실행하면 아래처럼 뜨는데 

Found Muse device, Connected to Muse Device 이거 뜨면 정상적으로 연결된거다!

이렇게 된 후에는 자동으로 10초간 정보가 기록되고

이렇게 csv 파일로 기록이 된다!

만약에 연결이 안되거나 하면 전원을 켜보거나 자신의 PC에 블루투스 동글이나 블루트스 연결할 수 있는 장치가 있는지 확인해보는걸 추천한다..

 

마무리

 

그리고 csv에 저장된건 사실 봐도 지금은 뭔지는 모르겠다.. 일단 연구 과제가 이걸로 뭐 UI 만들어서 기록하도록 만드는건데 암튼 이걸 할려고 좀 이상한 짓을 한 것 같다.. 그냥 파이썬을 커맨드 라인으로 실행해서 하는 것도 빨랐을지도.. (뭐 시간이 중요해서 이렇게 하는거지만..) 

 

아무튼 어쩌다 보니 10분안에 쓸려고 했었는데 1시간 30분 동안 쓰고 있다.. 

이 외의 오류는 저도 해결은 잘 못하겠지만.. 암튼 잘해보시길요..

 

오늘 수업 있는데.. 왜 시계에 오전 4:38 이라고 떠있는거지..

 

그리고 위에 말한 것 처럼 이 이외에 오류나는건 나도 모른다.

반응형
반응형

이제 선택으로 제어되도록 해야한다!!

이건 수학적 지식이 전혀 필요없어서 엄청 간단했다..!

 

자 일단 내가 구상하고 있는건 두번째 끝점을 클릭한 상태로 드래그 하면 IK로 움직이는 것이고, IK의 중간 점을 눌르면 방향이 반대로 되서 IK가 다시 계산되도록 하는 것이다..!

 

이걸 구현할려면 버튼이 클릭됬는지 안됬는지를 감지해야한다..

근데 그냥 버튼이 클릭됬는지 안됬는지를 감지하면 안된다. 이게 문제가.. 버튼을 눌렀는지 감지하는 방식이 2가지 정도가 있는데.. 마우스의 위치를 감지하면서 마우스의 위치가 내가 클릭할려는 요소 위에 있는지 감지하는 방법이 있고.. 요소 자체를 클릭했는지 뗐는지를 감지하도록 하는 방법이 있다..

근데 첫번째 방법의 경우는 마우스 아래 그 버튼이 있는지 계산하는게 알고리즘이 필요해서 구현하기는 힘들어서 넘어가고.. 그 요소를 클릭했는지 뗐는지를 감지하도록 해야한다..

 

근데 여기에서 한가지 문제가 생긴다.

끝점을 제어할 것이기 때문에 끝점을 눌르는 것까지는 문제가 안된다..

문제가 되는건 뗄때 인데.. 내가 끝점을 클릭하고 드래그 할땐 IK 계산이 되다가 마우스 클릭을 떼면 IK 계산이 풀리도록 해서 되게 하는 방식인데 요소를 클릭했는지 뗐는지 감지하는건 마우스가 요소 그 위에 있을 때만 감지가 가능하다..

 

그래서 감지를 할려면 항상 어떤 요소가 끝 점을 따라다니다가 마우스로 클릭하면 마우스를 따라오다가 뗐는지 감지하면 다시 끝점으로 돌아가도록 하면서 감지해야한다. (끝점을 따라가야하는 이유는 그 요소에만 마우스 감지 기능을 넣을 것이기 때문이다.. 그래서 끝점을 안따라다니면.. 다시 움직일 때는 그 요소의 위치에 마우스를 두고 클릭해야 움직이게 할 수 있다..)

그러기 위해선 새로운 요소를 만들어야 한다. 끝점과 똑같은걸로

 

일단 joint_2_mouse_point로 만들었는데 이것에 대해 클릭과 클릭이 떼지는걸 감지해서 다른 함수로 넘겨주는 코드를 적어준다. 아래 코드는 InitJoint 함수를 실행하면서 실행되도록 했다.

joint_2_mouse_point.PointerPressed({ this, &MainPage::JointControlJoint2PointerPressed }); // 조인트 2의 포인트를 클릭 했는지
joint_2_mouse_point.PointerReleased({ this, &MainPage::JointControlJoint2PointerReleased }); // 조인트 2의 포인트를 클릭 안했는지

그리고 항상 끝점을 따라가도록 UpdatePos를 실행할 때 마다 팔의 끝점으로 위치를 바꾸도록 했다.

void MainPage::UpdatePos() {
    // 파란색 끝 포인트의 위치 변경
    ChangePosPoint(joint_2_point, ControlJoint(), joint_2_point_xy);
    ChangePosLine(joint_2_line, joint_2_point_xy);

    // 파란색 끝 마우스 클릭 감지 포인트의 위치 변경
    ChangePosPoint(joint_2_mouse_point, ControlJoint(), joint_control_mouse_point_pos); // // 마우스 포인터를 따라가게

    // 파란색의 첫 포인트 위치 변경
    ChangePosLine(joint_2_line, joint_1_point_xy, false);

    // 빨간색의 끝 포인트 위치 변경
    ChangePosPoint(joint_1_point, ControlJoint(), joint_1_point_xy);
    ChangePosLine(joint_1_line, joint_1_point_xy);
}

 

그 후엔 그 요소가 클릭됬는지 클릭에서 뗐는지를 감지할 때 마다 어떤 함수를 실행하니 그 함수를 만들어야 한다.

일단 그 함수에서는 마우스가 클릭되었는지 안되었는지 확인을 한다. 이유는 드래그를 하고 있는지 없는지를 판단해야하기 때문이다. 그리고 마우스를 뗐을 때 UpdatePos 함수를 한번 실행하는데.. 이것의 이유는 이후에 알 수 있다. (주석에 나와있긴 하다)

 

일단 코드는!

void MainPage::JointControlJoint2PointerPressed(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    is_joint_2_point_click = true; // 마우스가 클릭 되었다면
}

void MainPage::JointControlJoint2PointerReleased(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    is_joint_2_point_click = false; // 마우스가 클릭 되지 않았다면

    UpdatePos(); // 마우스를 떼고 마우스를 움직여야 돌아가기 때문에.. 이걸 방지하기 위해 미리 한번 업데이트 시킴
}

이제 이렇게 한 후.. 마우스가 드래그 되고 있을 때 IK가 계산되도록 해야하기 때문에 캔버스에서 마우스의 움직임을 가져오는 함수에서 수정을 해야한다.

 

여기에서도 간단하게, 마우스가 드래그 되고 있을 때만 IK를 계산하고 업데이트 시켜야 하니 그냥 if문으로 is_joint_2_point_click이 true일 때만 되도록 했다.

void MainPage::JointControlCanvasPointerMoved(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    Input::PointerPoint point = e.GetCurrentPoint(ControlJoint());
    joint_control_mouse_point_pos = { point.Position().X, point.Position().Y };

    Point_X().Text(to_hstring(joint_control_mouse_point_pos.x));
    Point_Y().Text(to_hstring(joint_control_mouse_point_pos.y));

    // 마우스 포인터가 클릭 되었을 때만 계산하고 업데이트 하도록
    if (is_joint_2_point_click) { 
        CalIKPos(joint_control_mouse_point_pos);
        UpdatePos();
    }
}

근데 이걸로 되긴 한다.. 근데 한가지 문제가 발생한다.. 마우스를 드래그 하는 것 까지는 좋은데.. 

아까 위에서 말한 것 처럼 마우스에서 클릭을 뗐을 때 다시 끝점으로 안돌아간다.. 이렇게 되면 그 요소만 클릭할 수 있어서 그 요소를 클릭해야지만 움직이게 할 수 있다.. 그래서 이걸 위해 끝점이 클릭 상태가 아니라면 끝점을 따라가고 있게 바꿨다. 그래서 코드는 이렇게!

void MainPage::UpdatePos() {
    // 파란색 끝 포인트의 위치 변경
    ChangePosPoint(joint_2_point, ControlJoint(), joint_2_point_xy);
    ChangePosLine(joint_2_line, joint_2_point_xy);

    // 파란색 끝 마우스 클릭 감지 포인트의 위치 변경
    if(!is_joint_2_point_click) // 끝점이 클릭되지 않았다면
        ChangePosPoint(joint_2_mouse_point, ControlJoint(), joint_2_point_xy); // 끝점을 따라가고 있게
    else // 끝점이 클릭되고 있다면
        ChangePosPoint(joint_2_mouse_point, ControlJoint(), joint_control_mouse_point_pos); // 마우스 포인터를 따라가게

    // 파란색의 첫 포인트 위치 변경
    ChangePosLine(joint_2_line, joint_1_point_xy, false);

    // 빨간색의 끝 포인트 위치 변경
    ChangePosPoint(joint_1_point, ControlJoint(), joint_1_point_xy);
    ChangePosLine(joint_1_line, joint_1_point_xy);
}

if else로 간단하게 해결할 수 있었다. if에서 처음을 false로 해서 한 이유는 일단 형식상으론 마우스의 끝점이 클릭되고 있는건 특별 경우라서.. 그렇게 했다. 

 

암튼 이걸로 클릭하면 아래처럼 위치가 바뀐다..! (마우스 따라올 때 흰색 점도 같이 따라오는건.. 확인하기 위해 한 것이고 나중엔 투명하게 바꿀 것이다.)

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

아주 잘 작동된다..!

 

이제 구현할 부분은 중간 점을 눌르면 반대의 각도로 바뀌는 것이다.. 어쨋든 내가 한 방식으로 한다면..

일단 아까처럼 InitJoint 함수에 2가지 클릭하는 것과 떼는 것 2가지를 추가한다.

joint_1_point.PointerPressed({ this, &MainPage::JointControlJoint1PointerPressed }); // 조인트 1의 포인트를 클릭 했는지
joint_1_point.PointerReleased({ this, &MainPage::JointControlJoint1PointerReleased }); // 조인트 1의 포인트를 클릭 안했는지

이런식으로 추가한 후에는 각 함수를 만드는데

어떤 식으로 마우스로 클릭했는지 감지하는 것이 문제이다..

방법이라면 그 자리에서 마우스를 떼는 것을 감지했을 때 바뀌도록 하는 방법이 있지만.. 이것의 문제는 다른 곳에서 마우스를 클릭하고 그곳에서 떼면 이것도 감지가 된다..

그리고 마우스를 그곳에서 클릭했을 때 감지되는건.. 또 너무 빠르다.. 왜냐면 잘 못 눌렀을 때 눌른 상태로 드래그 하면 선택이 안되는걸 원해서..

 

암튼 그래서 내가 원하는건 버튼에서 마우스를 클릭하고 그 버튼에서 마우스를 떼었을 때를 감지해서 일을 처리하도록 하는 것이다.

 

이걸 구현하는건 간단하다. 눌렀을 때 A변수를 true로 바꿔주고 떼는 것을 감지하는면 A변수가 true인지 확인하고 true이라면 함수를 실행하고 A변수를 false로 바꿔주면 된다..!

 

그렇기에 바로 구현!

void MainPage::JointControlJoint1PointerPressed(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    is_joint_1_point_click = true; // 마우스가 클릭 되었다면
}

void MainPage::JointControlJoint1PointerReleased(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    // 중간점을 클릭했을 경우 중간의 각도를 반대로 바꿔서 계산하게 해야함으로 그 버튼만 클릭하고 떼었는지 확인할려고 if문 함
    // 확인이 되면 마지막 포인트로 다시 IK를 계산하고 위치를 업데이트 시키고 다시 클릭을 뗀걸로 인식하도록 바꾼거
    if (is_joint_1_point_click) {
        joint_angle_reverse = !joint_angle_reverse; // 각도를 바꾸면 그 뒤로도 고정되서 계산되게 해야함으로 !로 true false가 서로 토글로 바뀌도록
        CalIKPos(joint_2_point_xy); 
        UpdatePos();
        is_joint_1_point_click = false; // 마우스가 클릭 되지 않았다면
    }
}

근데 여기에서 joint_angle_reverse 라는 것이 보인다. 이것의 경우 현재 중간 점의 각도가 반대로 계산할지 정상으로 계산할지 하는 변수로 했는데.. 

반대로 계산하는건 IK를 계산하는 부분에서 acos 앞에 -만 붙여서 계산하는 것이기 때문에 이후에도 계속 고정으로 계산되게 하기 위해서 필요한 변수이다. 물론 IK를 계산하는 함수에 집어넣는 방식으로 할 수도 있지만.. 내가 생각하는 형식상 맞지도 않고 굳이 또 넣으면 함수를 수정해야하기 때문이다..

 

그래서 저 변수로 각도가 조정되게 하고, 여기에서 처럼 토글을 구현할 경우에는 bool 값에는 !를 붙이면 된다. 

이유는 !는 반대로 만들어주는건데 !true -> false이고 !false -> true로 되기 때문에 앞에 !로 붙이는 것이다.

 

그 후에 CalIKPos와 UpdatePos를 붙여주는 이유는 CalIKPos와 UpdatePos가 구동되는 조건은 마우스가 움직일 때만이다. 근데 이건 클릭된 것을 감지하면 바로 바뀌어야 하는데.. 안되기 때문에 저기에도 추가해서 바뀌도록 하는 것이다.

 

암튼 이렇게 바꾼 후에는 CalIKPos에서도 IK 계산을 바꿔야 하는데 어차피 joint_angle_reverse 변수로 알 수 있기 때문에 이걸로 판단할 수 있도록 했다!

theta2 = acos((pow(mouse_x, 2) + pow(mouse_y, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));

if (joint_angle_reverse) // 중간 점의 각도를 반대로 돌리는 것
    theta2 = -theta2;

이런식으로

원래 생각은 if와 else로 theta2 구하는 공식을 acos와 -acos 2개로 만들려고 했다. 

근데 그냥 생각해보니 어차피 -만 붙이는거라서 구한 값에 -를 붙일지 말지로 쉽게 구현할 수 있어서 그걸로 바꿨다..!

 

근데.. 해보니깐 잘되긴 한다.. 근데 한가지 문제가 생겼다.. 중간 점을 클릭하면 바뀌긴 하는데.. theta2 값을 확인하니 바뀔 때마다 약간의 오차가 발생한다.. 이 경우는 수학적이라서.. 어차피 계산에는 큰 영향을 안줄 것 같아서 놔두긴 했는데.. theta2값이 완전이 .000000으로 떨어지는 경우가 있다. 그곳에서 중간점을 눌르거나 하면.. 그냥 값이 계산이 안되고 고장나버린다.. 그래서 이유를 못 찾고 있다가..

옆에 있는 나보다 수학 잘하는 친구(이 문제도 그 친구가 테스트로 가지고 돌려보다가 발견한거..)가 무슨 이유인지 찾아볼려다가 내가 삼각함수에서 계산이 안되면 이런 문제가 일어난다고 하고 atan2 부분에서 문제가 일어나는 것 같다고 하니깐.. atan2에서 분모가 0이 될 수 없는데.. 0이 되버리면.. 무한의 값.. 뭐라면서 알려줬다.. 그래서 바로 나도 생각났다.. 이전에 a + b가 c보다 같거나 작을 때 계산한다고 했었는데.. 이걸 하기전에 본건.. 같거나 일 경우가 없었긴 했다.. 그래서 원래는 c <= a + b가 조건이고 이게 참일 때마다 IK를 계산하도록 했는데 이걸 c < a + b로 바꾸니.. 성공적으로 해결이 되었다..!  

 

그렇게 되어서 CalIKPos의 전체코드는 이렇다..!

void MainPage::CalIKPos(float2 point) {
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이
    float c = distance(center_point_xy, point); // 첫점과 끝점을 이은 길이

    // https://youtu.be/IKOGwoJ2HLk
    // x2, y2는 끝점 x1, y1은 중간점 x0, y0은 센터점

    // center_point_xy의 x와 y로 빼는 이유는 IK를 구현하는 곳이 250, 250인 중앙이여서 이걸 0, 0으로 옮기기 위해
    float mouse_x = point.x - center_point_xy.x;
    float mouse_y = point.y - center_point_xy.y;

    float theta1 = 0, theta2 = 0;
    float x2 = 0, y2 = 0;
    // a와 b의 최대 길이는 a + b가 됨, 그리고 c는 빗변이 되고 a + b를 넘으면 안됨.. 넘으면 삼각함수로 계산이 불가함, 그렇기에 미리 맞는지 확인하는 것
    if (c < a + b) { // 원래는 c <= a + b로 했는데.. 이렇게 할 경우 tan으로 구할 때 각도가 둘다 90도일 경우 터짐.. 그래서 그걸 막기 위해 완전히 같은 것도 막음
        // IK를 구현하기 위한 각도 계산
        theta2 = acos((pow(mouse_x, 2) + pow(mouse_y, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));

        if (joint_angle_reverse) // 중간 점의 각도를 반대로 돌리는 것
            theta2 = -theta2;

        theta1 = atan2(mouse_y, mouse_x) - atan2(b * sin(theta2), a + b * cos(theta2));

        // 위에서 theta1구할 때 theta1 + B에서 B를 뺐을 때 그 B 값을 theta1으로 사용해서 밑변과 높이를 구할 수 있긴 하지만.. 
        // 굳이 필요 없는 계산을 해야하기도 하고.. 굳이 할 필요가 없기 때문에 그냥 마우스 포인트로 되게 함
        x2 = mouse_x + center_point_xy.x;
        y2 = mouse_y + center_point_xy.y;
    }
    else {
        // 마우스 포인트와 선을 이어서 삼각형을 만들고 theta1의 각도를 얻어서 
        // 마우스 포인트와 선을 이은거에서 중간점은 구하는 방식이 같기 때문에 넘기고, 끝점을 구하는게 목적이기 때문에
        // 끝점을 구하기 위해 쫙 폈을 때 a + b이기 때문에 그걸로 밑변과 높이를 구하는거
        theta1 = atan2(mouse_y, mouse_x);

        x2 = (a + b) * cos(theta1) + center_point_xy.x;
        y2 = (a + b) * sin(theta1) + center_point_xy.y;
    }

    // 중간 점의 x와 y를 cos, sin으로 구하는 것
    float x1 = a * cos(theta1) + center_point_xy.x; // x, y를 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
    float y1 = a * sin(theta1) + center_point_xy.y;

    joint_1_point_xy = { x1, y1 };
    joint_2_point_xy = { x2, y2 };

    char debug[512];
    sprintf_s(debug, "theta1 = %f, theta2 = %f", theta1 * 180.0 / M_PI, theta2 * 180.0 / M_PI);
    debugText2().Text(to_hstring(debug));
}

 

그래서 이렇게 해서 IK 시뮬레이션 부분에선 각도 제한하는거 빼곤 전부 완성시켰다..!

그래서 결과물은 이렇다..!

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

암튼 끄읕..!

내 옆에 있던 친구는.. 내가 만드는거 자랑할 때마다.. 긍정적으로 봐주는 친구인데..! 매번 테스트할 때마다 짜잔하면 신기한건지.. 몇분을 움직여보기도 한다.. (오류를 찾아낼려고 하는 것일 수도 있지만..) 암튼.. IK를 고민할 때도 계속 도와줄려고 했었기도 했다..! 내년엔.. 다른 대학교로 간다는데.. 좀 아쉽긴 하다..

반응형
반응형

이전 글에서 말했 듯이 계산 범위를 넘어갔을 때 else로 처리한다고 했던 것에 대한 내용이다.

 

내가 생각해본 이론 설명

자세하게 말한다면 IK를 구현할려면 a, b, c가 필요하다.

a가 첫번째 팔 길이, b가 두번째 팔 길이이고 c가 a와 b의 삼각형에서 빗변이다.

사진으로 보면 이렇다.

근데.. 여기에서 마우스 포인터의 x, y와 센터점까지 길이가 c가 될 수 있는데 a와 b가 정해져 있으므로 c가 a + b를 벗어나면 삼각함수에서 계산이 전혀 되지가 않는다. 아마 코딩하면 nan(ind)인가 이걸로 값이 뜰 것이다..

 

그래서 이 경우 내가 생각한 해결 방법으론 c가 a + b를 넘어갔을 때 그냥 a와 b를 쫙 펼치고 삼각함수로 하든 어떻게든 해서 마우스쪽의 방향으로 IK를 돌려버리는 것을 생각했다..

 

그래서 일단 다시 샤프를 잡고 쫙 펴졌을 때를 그린 후에 삼각형을 찾아봤다.

그러더니.. 한가지 나왔다..

일단 빨간색 점의 경우 마우스 포인터의 위치이다. 

그래서 마우스 포인터의 위치로 직각삼각형을 그릴 수 있다..! 이걸 이용하면 밑변과 높이인 x, y를 구할 수 있다는 것..!

어쨋든 이전 글에서 θ1을 구할 때

float theta1 = atan2(mouse_y, mouse_x) - atan2(b * sin(theta2), a + b * cos(theta2));

 이런 공식을 썼다. 근데 이 공식이 θ1 + B에서 B인 atan2(b * sin(theta2), a + b * cos(theta2)) 이걸 그냥 뺀거다. 

그렇기 때문에 θ1 + B가 필요한 각도이다..!

 

그래서 그냥 theta1 = atan2(mouse_y, mouse_x)를 가져다 썼다.

그럼 이걸로 θ1을 구했기 때문에 이걸로 밑변과 높이를 구할 수 있게 된다.

 

근데 여기에서 필요한건 마우스 포인터의 위치가 아니라 a의 끝점과 b의 끝점의 x, y 좌표가 필요하다.

그래서 이 x, y 좌표를 구하기 위해 또 삼각형을 찾아보면

이런식으로 직각삼각형을 또 그릴 수 있게 된다. (일단 θ1 + B는 설명하기 쉽게 θ1을 바꿨다)

그런데 이전에서 θ1을 구했기 때문에 a 부분의 삼각형과 b부분의 삼각형의 밑변과 높이를 구할 수 있게 된다..!

 

계속 말하고 있지만..

밑변을 구하는건 빗변 * cos(θ1)이고 높이는 빗변 * sin(θ1)이기 때문에 각 삼각형의 밑변 높이를 구한다면??

a의 밑변과 높이를 구할 경우 a * cos(θ1), a * sin(θ1)으로 구할 수 있고 b의 밑변과 높이를 구할 경우 어차피 쫙 펴져있는 경우에만 구하는거라서 (a + b) * cos(θ1), (a + b) * sin(θ1)으로 구할 수 있게 된다.

 

어쨋든 밑변은 x 높이는 y로 되기 때문에 이걸 C++로 구현해보면

float theta1 = atan2(mouse_y, mouse_x);

float x2 = (a + b) * cos(theta1);
float y2 = (a + b) * sin(theta1);

이렇게 된다. x2, y2로 한 이유는 두번째 팔의 끝점을 구하는 거기 때문에.. 첫번째 팔의 끝점은 x1, y1..

 

이론 적용

암튼 여기에서도 계산했던 곳이 센터가 0, 0일 경우일 때 구한 것이기 때문에 각 x2, y2에 중앙으로 갈 수 있게 값을 더 해준다!

float mouse_x = point.x - center_point_xy.x;
float mouse_y = point.y - center_point_xy.y;

float theta1 = atan2(mouse_y, mouse_x);

float x2 = (a + b) * cos(theta1) + center_point_xy.x;
float y2 = (a + b) * sin(theta1) + center_point_xy.y;

그럼 이렇게 되면 각 필요한 x, y좌표가 구해진다..!

 

이론 적용 및 코드 수정

근데 이제 여기에서 이전 글에서 한 코드에 그대로 넣으면.. 코드에서 중복 코드가 많아진다.. 그래서 그 중복 코드는 없애고 고친결과..! 이렇게 완성되었다!

void MainPage::CalIKPos(float2 point) {
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이
    float c = distance(center_point_xy, point); // 첫점과 끝점을 이은 길이

    // https://youtu.be/IKOGwoJ2HLk
    // x2, y2는 끝점 x1, y1은 중간점 x0, y0은 센터점

    // center_point_xy의 x와 y로 빼는 이유는 IK를 구현하는 곳이 250, 250인 중앙이여서 이걸 0, 0으로 옮기기 위해
    float mouse_x = point.x - center_point_xy.x;
    float mouse_y = point.y - center_point_xy.y;

    float theta1 = 0, theta2 = 0;
    float x2 = 0, y2 = 0;
    // a와 b의 최대 길이는 a + b가 됨, 그리고 c는 빗변이 되고 a + b를 넘으면 안됨.. 넘으면 삼각함수로 계산이 불가함, 그렇기에 미리 맞는지 확인하는 것
    if (c <= a + b) {
        // IK를 구현하기 위한 각도 계산
        theta2 = -acos((pow(mouse_x, 2) + pow(mouse_y, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
        theta1 = atan2(mouse_y, mouse_x) - atan2(b * sin(theta2), a + b * cos(theta2));

        // 위에서 theta1구할 때 theta1 + B에서 B를 뺐을 때 그 B 값을 theta1으로 사용해서 밑변과 높이를 구할 수 있긴 하지만.. 
        // 굳이 필요 없는 계산을 해야하기도 하고.. 굳이 할 필요가 없기 때문에 그냥 마우스 포인트로 되게 함
        x2 = mouse_x + center_point_xy.x;
        y2 = mouse_y + center_point_xy.y;
    }
    else {
        // 마우스 포인트와 선을 이어서 삼각형을 만들고 theta1의 각도를 얻어서 
        // 마우스 포인트와 선을 이은거에서 중간점은 구하는 방식이 같기 때문에 넘기고, 끝점을 구하는게 목적이기 때문에
        // 끝점을 구하기 위해 쫙 폈을 때 a + b이기 때문에 그걸로 밑변과 높이를 구하는거
        theta1 = atan2(mouse_y, mouse_x);

        x2 = (a + b) * cos(theta1) + center_point_xy.x;
        y2 = (a + b) * sin(theta1) + center_point_xy.y;
    }

    // 중간 점의 x와 y를 cos, sin으로 구하는 것
    float x1 = a * cos(theta1) + center_point_xy.x; // x, y를 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
    float y1 = a * sin(theta1) + center_point_xy.y;

    joint_1_point_xy = { x1, y1 };
    joint_2_point_xy = { x2, y2 };

    char debug[512];
    sprintf_s(debug, "theta1 = %.2f, theta2 = %.2f", theta1 * 180.0 / M_PI, theta2 * 180.0 / M_PI);
    debugText2().Text(to_hstring(debug));
}

저기 주석에서도 말하는 것 처럼 c <= a + b가 성립이 될 때 그냥 마우스 포인터로 하는 이유는 θ1 + B를 구해서 할 순 있긴 하지만.. 이 경우 새로운 변수도 만들어야 하고.. 굳이 계산을 해서 필요없는 계산을 하게 하기 싫어서가 이유이기도 하다..

 

결과

그래서 이걸로 된 결과물은 이렇다!!

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

엄청 잘 된다!!!

 

2일 동안의 공부는 잘한 것 같다!

이거는.. 이전 글 쓰고.. 바로 계산해서 1시간 안에 찾고 고친걸 적고 있는건데.. 물론 간단한 식인건 맞지만.. 빠른 속도로 1시간 안에 이걸 구하고 바로 적용한게.. 2일동안 허무한 공부를 한 보람이 있는 것 같다..! 만약 그 공부가 없었다면 이걸 1시간 안에 해내지 못했을 것이다.. 왜냐면.. 공부를 하기전에는.. 라인을 그려서 유클리드 거리로.. 어떻게든 해서 두번째 x, y를 구해볼까 생각했었는데.. 지금 생각해보니.. 만약 그렇게 했다면 각도를 알 수 없어서 두번째 x, y를 전혀 구할 수 없었을 것이다.. 뭐.. 다른 방법은 있겠지만.. 내가 생각한 방법도 맞는 것 같다..! 

 

계속 하면서 삼각형은 진짜.. 대단한 도형이라고 생각된다.. 삼각형으로 내가 필요한 대부분의 것을 구현하거나 얻을 수 있다는 것이.. 진짜 너무나도 신기하다..

 

암튼 까먹지 않을려고 구하고 적용하자마자 쓰다보니.. 새벽 1시 25분.. 바로 자야겠다..

 

암튼 끝!!

반응형
반응형

이전글에서는 아쉽게도.. Inverse Kinematics를 구현하기에.. 실패했다..

반은 되고 반은 안되고.. 암튼 그래서.. 유튜브를 찾아봤는데.. 

잘 정리된 영상이 있었고 식도 모두 있었다..!

 

식 찾기

https://youtu.be/IKOGwoJ2HLk

여기에서 식을 찾았는데 

이 식이다.

또한 팔을 반대로 접을 경우에는 q2에 cos 앞에 -만 붙이면 중간의 팔 각도가 반대로 바뀌어서 계산을 시킬 수가 있다.

이걸 이용하면 내 로봇에는 움직일 수 있는 각도가 정해져있는데.. 이 각도가 q2라면 q2가 벗어났을 때 cos값 앞에 -만 붙여서 목표 지점에 도달 할 수 있도록도 할 수 있을 것이다.. 물론.. 어렵겠지만.. 

 

IK를 코드로 구현

아무튼 이 공식으로 하기로 했다..!

근데 이 공식을 안해본건 아니였다..

이전 글에서 했던대로 너무 되지가 않아서 저 식으로 해봤는데.. 아쉽게도 작동을 이상하게 했다.. 근데 부모님에게 물어보다가 이 식을 말했더니.. 내가 했던게 tan -1이 역탄젠트여서 atan으로 했는데.. 부모님이 말하기론.. atan2로 해야한다고 한다.. 무슨 분모가 0이 될 수가 없는데 atan2는 뭐 어떻게 한다 해서.. 그걸로 했었는데 됬었다..!!

 

일단 그 복잡한 식은 전부 버리고.. 단 2개의 식만이 필요해서.. 좀 놀라웠는데.. C++로 변경하면

float theta2 = -acos((pow(x2, 2) + pow(y2, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
float theta1 = atan2(y2, x2) - atan2(b * sin(theta2), a + b * cos(theta2));

이렇게 구할 수 있다. 여기에서 q2는 theta2이고 q1은 theta1이다.

굉장히 간단한 식인데.. 그런 놀라운 계산을 할 수 있다는게.. 엄청나다.. 

 

암튼 이걸 이용해서 결국에는 중간점의 x, y 좌표를 구해야하기 때문에 θ1의 값으로 밑변과 높이를 구해야한다..!

이전 글과 마찬가지로 첫번째 팔의 길이가 l이면 l * cos(θ1)으로 밑변을 구할 수 있고 l * sin(θ1)으로 높이를 구할 수 있다..! 그리고 밑변은 x가 되고 높이는 y가 된다..! 

그래서 그걸로 C++에서 구한다면 이렇게!!

float x1 = a * cos(theta1);
float y1 = a * sin(theta1);

 

근데 여기에서 한가지 문제가 발생한다. 이전 글에서도 말했 듯이.. IK를 계산해야하는 위치는 캔버스의 크기인 500 * 500의 중심인 250, 250에서 계산을 해야한다. 근데 이 상태로 계산하면 삼각함수에서 오류가 나서 값을 못 구하게 될 것이다..

그래서 이 식의 계산했던 곳은 센터 점이 0, 0이기 때문에 250, 250에서 0, 0으로 이동시켜야 한다.

근데.. 사실 여기에서 또 문제가 발생하긴 한다.. x값은 왼쪽에서 오른쪽으로 커지니깐.. 이건 상관없는데.. y값은 원래라면 아래쪽에서 위로 커져야 하지만.. 이것의 첫 0, 0이 왼쪽 상단이기 때문에.. y 값을 반전 시켜서 계산 시켜야 하긴 한다..

근데 그러면 계산할 때 식도 어느정도 길어질 수도 있고 하고.. 그냥 y값만 반대로 된거기 때문에.. 정상적으로 작동하는데 팔의 각도가 반대로 되어있다.. θ2에서 acos 앞에 -를 붙이면 같아지긴 한다.. 그래서 그냥 반대로 되고 정상적으로 작동하긴 해서 그냥 이대로 하기로 했다.

 

어쨋든 두번째 문제는 무시하고 첫번째 문제를 해결할려면 중간 점의 x, y좌표를 각각 빼주고 나중에 더해줘야 한다. C++ 코드로 구현한다면 이렇게 된다.

float x2 = point.x - center_point_xy.x;
float y2 = point.y - center_point_xy.y;
            
float theta2 = -acos((pow(x2, 2) + pow(y2, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
float theta1 = atan2(y2, x2) - atan2(b * sin(theta2), a + b * cos(theta2));
            
float x1 = a * cos(theta1) + center_point_xy.x;
float y1 = a * sin(theta1) + center_point_xy.y;

여기에서 center_point_xy는 말그대로 중심점의 x, y다 각 값은 그냥 250, 250 이게 있는거다. 그래서 250, 250으로 뺐다가 나중에 더하는 방식으로 되고 이렇게 하는 이유가 캔버스의 크기를 바꾸면 자동으로 되게 하기 위해서이다...!

 

복잡한 코드를 바꾸기!

그리고 이전 글에서 나온 것 처럼 원래 하던 방식은..

UpdatePos에서 각도를 구하고 x, y좌표를 구해서 직접 업데이트 하는 방식인데.. 이 경우 함수화를 해서 나중에 재사용할 때 조금 문제가 생길 수 있다.. 필요한거.. 다시 만들어야 하고.. 그래서 CalIKPos라는 함수를 만들었는데.. 여기에서 각 포인트의 x, y좌표를 구해서 넣어놓고 각 포인트의 x, y좌표는 전역 변수이기 때문에 UpdatePos에서 그 점으로 라인이나 점을 이동시키는 코드로 변경했다.

 

그래서 모든 코드를 아래처럼 고쳤다.

void MainPage::InitJoint() {
    double point_width = 20;
    double point_height = 20;
    double line_thickness = 10;

    // 센터 점 위치 초기화
    center_point_xy = { (float)(ControlJoint().Width() / 2), (float)(ControlJoint().Height() / 2) }; // 기본적으로 Xaml에서 나온 값이 double이므로 float로 변환
    // 초기 상태 IK 계산
    CalIKPos({ center_point_xy.x, center_point_xy.y - joint_1_line_size - joint_2_line_size });

    // 컨트롤러 중심의 고정 센터 포인트를 표시 (그냥 참고용임)
    DrawPoint(center_point, ControlJoint(), point_width, point_height, center_point_xy);
    // 첫번째 조인트 선
    DrawLine(joint_1_line, ControlJoint(), line_thickness, center_point_xy, joint_1_point_xy, Xaml::Media::SolidColorBrush(Colors::Red()));
    // 컨트롤러 조인트 1 끝의 점
    DrawPoint(joint_1_point, ControlJoint(), point_width, point_height, joint_1_point_xy);
    // 두번째 조인트 선
    DrawLine(joint_2_line, ControlJoint(), line_thickness, joint_1_point_xy, joint_2_point_xy, Xaml::Media::SolidColorBrush(Colors::Blue()));
    // 컨트롤러 조인트 2 끝의 점
    DrawPoint(joint_2_point, ControlJoint(), point_width, point_height, joint_2_point_xy);

    joint_2_point.PointerPressed({ this, &MainPage::JointControlJoint2PointerPressed }); // 조인트 2의 포인트를 클릭 했는지
    joint_2_point.PointerReleased({ this, &MainPage::JointControlJoint2PointerReleased }); // 조인트 2의 포인트를 클릭 안했는지
    ControlJoint().PointerMoved({ this, &MainPage::JointControlCanvasPointerMoved }); // 캔버스의 마우스 포인터 위치를 얻기 위한거
}

void MainPage::CalIKPos(float2 point) {
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이

    float c = distance(center_point_xy, point); // 첫점과 끝점을 이은 길이

    // a와 b의 최대 길이는 a + b가 됨, 그리고 c는 빗변이 되고 a + b를 넘으면 안됨.. 넘으면 삼각함수로 계산이 불가함, 그렇기에 미리 맞는지 확인하는 것
    if (c <= a + b) {
        // x2, y2는 끝점 x1, y1은 중간점 x0, y0은 센터점

        // center_point_xy의 x와 y로 빼는 이유는 IK를 구현하는 곳이 250, 250인 중앙이여서 이걸 0, 0으로 옮기기 위해
        float x2 = point.x - center_point_xy.x;
        float y2 = point.y - center_point_xy.y;

        // IK를 구현하기 위한 각도 계산
        float theta2 = -acos((pow(x2, 2) + pow(y2, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
        float theta1 = atan2(y2, x2) - atan2(b * sin(theta2), a + b * cos(theta2));

        // 중간 점의 x와 y를 cos, sin으로 구하는 것
        float x1 = a * cos(theta1) + center_point_xy.x; // x, y를 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
        float y1 = a * sin(theta1) + center_point_xy.y;

        joint_1_point_xy = { x1, y1 };
        joint_2_point_xy = point;

        char debug[512];
        sprintf_s(debug, "theta1 = %.2f, theta2 = %.2f", theta1 * 180.0 / M_PI, theta2 * 180.0 / M_PI);
        debugText2().Text(to_hstring(debug));
    }
    else { // 여기에선 마우스가 a + b보다 넘게 벗어났으므로 마우스와 센터점을 연결해서 삼각형을 그리고 그걸로 각 지점의 위치를 구해야함

    }
}

void MainPage::UpdatePos() {
    // 파란색 끝 포인트의 위치 변경
    ChangePosPoint(joint_2_point, ControlJoint(), joint_2_point_xy);
    ChangePosLine(joint_2_line, joint_2_point_xy);

    // 파란색의 첫 포인트 위치 변경
    ChangePosLine(joint_2_line, joint_1_point_xy, false);

    // 빨간색의 끝 포인트 위치 변경
    ChangePosPoint(joint_1_point, ControlJoint(), joint_1_point_xy);
    ChangePosLine(joint_1_line, joint_1_point_xy);
}

void MainPage::JointControlCanvasPointerMoved(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    Input::PointerPoint point = e.GetCurrentPoint(ControlJoint());
    float2 point_pos(point.Position().X, point.Position().Y);

    Point_X().Text(to_hstring(point_pos.x));
    Point_Y().Text(to_hstring(point_pos.y));

    CalIKPos(point_pos);
    UpdatePos();

    char debug[512];
    sprintf_s(debug, "Center X: %.2f, Y: %.2f || Joint1 X: %.2f, Y: %.2f || Joint2 X: %.2f, Y: %.2f", center_point_xy.x, center_point_xy.y, joint_1_point_xy.x, joint_1_point_xy.y, joint_2_point_xy.x, joint_2_point_xy.y);
    debugText1().Text(to_hstring(debug));
}

나머지 ChangePosPoint이나 ChangePosLine이나 DrawLine, DrawPoint에 대한 함수는 이전 글에 있다!

 

암튼 debug는 내가 실시간으로 값을 확인할려고 적어둔 것이고

저기 중간에서 else 에서 비워둔 곳에.. 주석으로 쓰여있긴 하지만..

c 값이 a + b 보다 초과할 경우 삼각함수로 계산이 안되어서.. 이 경우엔 현재 계획중이지만 마우스와 센터점을 연결해서 삼각형을 그리거나 라인을 그려서 그 사이에 점들이 일정하게 들어가도록 할 예정이다.

 

결과

암튼 이렇게 해서 제작된 결과물은 이렇게 된다..!

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

이제 저기 안에서 제작할 부분은.. 끝점을 클릭한 상태로 드래그 할 때만 IK가 계산되도록 하는 것이고 중간 점을 클릭하면 θ2구할 때 acos앞에 - 붙여서 반대로 되게 해서 하는 것과 각도 제한시키는 것과 c값이 벗어날 경우 일자로 뻣어서 마우스 쪽으로 따라오도록 만들 것이다..! 

 

한가지 아쉬운건 IK를 구현하는 다른 방법이 생각나긴 했는데.. 이걸 한다고 해도 제대로 되지 않을 것 같고 그냥 이대로 하는게 가장 나을 것 같아서.. 이건 완성한 뒤에..! 할 수 있으면 해보는걸로 하기로 했다..!

 

암튼 이번건 여기에서 끄읕!!

반응형
반응형

이전 글에 이어서 연결됩니다..!

 

이제 프로그램을 제작할려는 이유 중 하나인 제일 어려운 IK(Inverse Kinematics)를 구현해야 된다..!

근데.. 이 것의 경우 수학이기 때문에.. 굉장히 어렵다.. 왜냐면.. 내가 수학을 거의 포기해서 고등학교에서도 수학이 10 ~ 30점 대였으니.. 거기에 지금은 고3이니깐.. (특성화고라서 3학년은 일반 교과목이 없다.)

 

근데.. 이번 기회로 수학을 좀 알아야 됬었다..

일단 수학으로 넘어가기 전에.. 사전 준비를 해야한다.. 그래픽으로 표시를 해야하기 때문에 일단 먼저 라인을 그려줘야 한다.

이전 글에서 일단 그리드를 그렸는데.. 로봇 팔을 그릴 때는 나중에 값을 쉽게 바꿔서 해야하기 때문에 Xaml에는 그리지 않고 C++로 전부 그리게 할 것이다.

 

IK를 적용하기 전 팔 그리기

그래서 일단 그려야 하는데..

한가지 문제는.. 여기에 있는 코드는 아직 확실하지가 않다.. 왜냐면 지금도 글을 쓰는데 IK 문제를 해결하고 있기 때문이다.. 일단 대부분 테스트 코드라서.. 확신하기는 어렵다..

// 필요한 전역 변수 
float joint_1_line_size = 100;
float joint_2_line_size = 100;

float2 center_point_xy = float2(250, 250); // 중앙 센터 포인트
float2 joint_1_point_xy = float2(250, 250 - joint_1_line_size); // 첫번째 조인트
float2 joint_2_point_xy = float2(250, 250 - joint_1_line_size - joint_2_line_size); // 두번째 조인트

Xaml::Shapes::Ellipse center_point = Xaml::Shapes::Ellipse(); // 센터에 표시용 점
Xaml::Shapes::Ellipse joint_1_point = Xaml::Shapes::Ellipse(); // 첫번째 조인트 끝 점 위치 제어
Xaml::Shapes::Ellipse joint_2_point = Xaml::Shapes::Ellipse(); // 두번째 조인트 끝 점 위치 제어
Xaml::Shapes::Line joint_1_line = Xaml::Shapes::Line();
Xaml::Shapes::Line joint_2_line = Xaml::Shapes::Line();
Xaml::Shapes::Line joint_3_line = Xaml::Shapes::Line(); // 마지막 집게 부분 회전시킬 수 있게 하는거

일단 이런식으로 지정했다. 내가 프로그램에 알려줄껀 로봇 팔의 각 길이이고, 최종적으로 움직여야 할 끝점의 좌표를 알려주고 나머지는 다 자동으로 계산해야하기 때문에 저렇게 변수를 만들었다.

일단 임시로 100, 100으로 되고 나중에는 로봇 팔의 길이에 따라서 각각 비율에 맞춰 500 * 500 안에 넣을 생각이다.

 

그 아래는 초기에 그냥 표시하기 용으로 할려고 각각의 점을 미리 위치를 지정한 것이다.

그리고 그 아래는 그냥 GUI에 표시하기 위한 제어할 수 있도록 하는 점이나 선들이다.

 

그리고 C++로 기본 위치를 표시해야한다. 나의 경우엔 아래 처럼 코드를 짰다.

void MainPage::DrawPoint(Xaml::Shapes::Ellipse& point, Xaml::Controls::Canvas& canvas, double point_width, double point_height, float2 pos, Xaml::Media::Brush color) {
    // 중심점 크기나 색 설정
    point.Width(point_width);
    point.Height(point_height);
    point.Fill(color);

    canvas.Children().Append(point);

    // 중심점 위치 설정
    Xaml::Controls::Canvas::SetLeft(point, pos.x - (point_width / 2));
    Xaml::Controls::Canvas::SetTop(point, pos.y - (point_height / 2));

    Xaml::Controls::Canvas::SetZIndex(point, 1);
}

void MainPage::DrawLine(Xaml::Shapes::Line& line, Xaml::Controls::Canvas& canvas, double thickness, float2 pos_1, float2 pos_2, Xaml::Media::Brush color) {
    // 위치나 색 설정
    line.X1(pos_1.x);
    line.Y1(pos_1.y);
    line.X2(pos_2.x);
    line.Y2(pos_2.y);

    line.StrokeThickness(thickness);
    line.Stroke(color);

    canvas.Children().Append(line);

    Xaml::Controls::Canvas::SetZIndex(line, 0);
}

void MainPage::InitJoint() {
    double canvas_width = ControlJoint().ActualWidth();
    double canvas_height = ControlJoint().ActualHeight();

    double point_width = 20;
    double point_height = 20;
    double line_thickness = 10;

    // 컨트롤러 중심의 고정 센터 포인트를 표시 (그냥 참고용임)
    DrawPoint(center_point, ControlJoint(), point_width, point_height, center_point_xy);

    // 첫번째 조인트 선
    DrawLine(joint_1_line, ControlJoint(), line_thickness, center_point_xy, joint_1_point_xy, Xaml::Media::SolidColorBrush(Colors::Red()));

    // 컨트롤러 조인트 1 끝의 점
    DrawPoint(joint_1_point, ControlJoint(), point_width, point_height, joint_1_point_xy);

    // 두번째 조인트 선
    DrawLine(joint_2_line, ControlJoint(), line_thickness, joint_1_point_xy, joint_2_point_xy, Xaml::Media::SolidColorBrush(Colors::Blue()));

    // 컨트롤러 조인트 2 끝의 점
    DrawPoint(joint_2_point, ControlJoint(), point_width, point_height, joint_2_point_xy);

    joint_2_point.PointerPressed({ this, &MainPage::JointControlJoint2PointerPressed }); // 조인트 2의 포인트를 클릭 했는지
    joint_2_point.PointerReleased({ this, &MainPage::JointControlJoint2PointerReleased }); // 조인트 2의 포인트를 클릭 안했는지
    ControlJoint().PointerMoved({ this, &MainPage::JointControlCanvasPointerMoved }); // 캔버스의 마우스 포인터 위치를 얻기 위한거
}

한가지 아쉬운점은.. 라인의 위치를 지정하는건 쉬운데.. Ellipse의 위치를 지정할 땐.. 캔버스 기준으로 지정해야하는게 조금 아쉬웠다.. 다른 방법도 있긴 했지만.. 저게 가장 쉽고 기본적인 방법인 것 같다.. 그리고 이전글에서 마우스 정보를 함수로 넘기는 것이 저 아래쪽에 추가를 했다. (초기 팔 위치가 잡혀야지 마우스 포인트 위치를 얻어서 제어를 할 수 있기 때문에.. 그리고 맨 끝점의 마우스를 클릭하는 것과 떼는 것도 넘겨야 한다. 지금은 구현을 안했지만.. 나중에 각 점을 클릭해서 어떤 작업을 수행하도록 해야하기 때문이다.

 

그렇게 해서 위 코드의 결과물은 이렇다..!

 

짜증나는 수학..

그러면 이제 준비를 완료 했으므로 수학으로 넘어가야 한다..

처음엔 내가.. 이걸 이해하기 위해 3일을 썼다.. 그래서 나만의 방식을 찾았고 그걸 구현했지만.. 결과로는 실패다..

반은 되는데 반은 안되었기 때문에..

그래서 만약 이 글을 참고한다면.. Y값이 양수나 음수 한쪽만 필요하다면 이 글의 방법을 참고하면 되지만.. 그게 아니라면 그냥 이후 글을 참고하던지.. 유튜브 영상을 보면 된다..

 

일단 내가 최대한 이해해서 정리한건 아래 사진이다.

여기에서 구해야할 값은 C로 표시되어있는 부분의 점의 좌표이다. b와 a가 만나는 지점의 좌표

 

일단 여기에서 고정되어있거나 기본적으로 계산 없이 알 수 있는 값은

코드의 결과물 사진 처럼 빨간색은 첫번째 팔(a)이고 파란색이 두번째 팔(b)과 초록색 선으로 표시된 x와 y이다.. 

각 a와 b는 팔의 길이를 가지고 있고.. x와 y는 파란색의 끝점의 좌표이다.

 

일단 여기에서 필요 없는 변수는 C와 θ2 이다.. 지금 내 방법으론 필요없다는거지.. 이후 글에서 나올 식에는 필요하다..

 

암튼 x, y좌표를 알고 있다면 c의 값을 쉽게 구할 수 있다. 센터 점에서 x와 y 값의 거리를 구하면 되기 때문이다.. 이때! 유클리드 거리 공식을 이용하면 된다..! 근데 WinRT에서 float2에 쪽에 기본적으로 distance 함수가 있어서 이걸 이용했다. distance 함수도 유클리드 거리 공식을 이용해서 float 값으로 나온다..!

 

그렇게 해서 c를 구하게 되고

위 사진처럼 대충 칠해놓은 것을 보면 삼각형인 것을 알 수가 있다..!

 

여기에서 초록색 삼각형에서 오른쪽 상단에 I 로 되어있는 값을 구해야한다..!

I 값의 공식은 물론 직각 삼각형이기 때문에 역코사인으로 I 값을 찾을 수 있다. 하지만 x, y, c 값을 이미 알고 있고 이 뒤에 계산해야할 것은 3개의 변의 길이로 구하는 것이기 때문에 함수화를 할려고 3개 변으로 각도를 구하는 방식으로 했다.

 

https://www.mathsisfun.com/algebra/trig-solving-sss-triangles.html

 

Solving SSS Triangles

Solving SSS Triangles "SSS" means "Side, Side, Side" "SSS" is when we know three sides of the triangle, and want to find the missing angles. To solve an SSS triangle: We use the "angle" version of the Law of Cosines: cos(C) = a2 + b2 − c2 2ab cos(A) = b2

www.mathsisfun.com

3개의 변으로 구할 땐 위 링크에서 처럼 SSS 방식을 이용해서 풀면 된다..! (분명 저거 중학교 때 열심히 외웠던 것 같은데..고1 수학 선행 때문에..)

 

그렇게 하면 I 값이 나오게 되고 아래 X 선과 위의 초록색 X선은 평행하기 때문에 왼쪽 아래에 B + θ1의 값은 I와 동일하다는 것이 된다..! 내가 이걸 기억하고 있어서 다른 영상에서는 안나오는 방법이지만.. 찾아내서 좋았는데.. 결국 이것 때문에.. 결과물이 이상해지긴 했다..

 

암튼 이제 I값과 동일하기 때문에 θ1을 구할려면 I 값에서 B 값을 빼면 θ1 값이 나오는 것이다..!

그리고 B 값도 아까 I 값을 구한 것 처럼 주황색 삼각형의 세 변을 알고 있고 내부 각을 알고 싶기 때문에 위 사이트에 나온 걸로 구하면.. B 값도 쉽게 구할 수 있다..!

 

그렇게 구하면 B를 알게 되고 I - B를 해서 θ1을 알게 된다..

 

그 후에

보라색으로 대충 칠한 부분을 보면 이것도 삼각형인걸 알 수 있다..!

그리고 여기에서는 알고 싶은건 a와 b가 만나는 점의 좌표를 구하고 싶은 것이기 때문에 θ1의 값이 필요했던 것이다.

암튼 그걸로 직각삼각형에서는 θ1 부분의 각도를 알면 밑변과 높이를 구할 수 있는데

밑변은 a * cos(θ1) 로 구할 수 있고 높이는 a * sin(θ1)으로 구할 수 있다.

그래서 밑변은 구할려는 값의 x가 되고 높이는 y값이 됨으로.. 이걸로 구할 수가 있다..!

 

그래서 이 모든 과정을 코드로 짜보았다..!

// SSS로 각을 구하는 것, 입력 값은 △ 이런 삼각형에서 오른쪽변, 왼쪽변, 밑변 순서로 입력하고 필요한 각도의 번호를 입력하는 것, a, b, c를 입력하고 b c사이의 각도를 구하고 싶다면 1
// 입력값 a, b, c / 구하고 싶은 각 bc: 1, ca : 2, ab : 3 으로
float MainPage::CalSSSRadian(float a, float b, float c, int angleNum) {
    float angle = 0;

    switch (angleNum) {
        case 1: // A
            angle = (pow(b, 2) + pow(c, 2) - pow(a, 2)) / (2.0 * b * c);
            break;
        case 2: // B
            angle = (pow(c, 2) + pow(a, 2) - pow(b, 2)) / (2.0 * c * a);
            break;
        case 3: // C
            angle = (pow(a, 2) + pow(b, 2) - pow(c, 2)) / (2.0 * a * b);
            break;
    }

    return acos(angle);
}

void MainPage::UpdatePos() {
    float x = joint_2_point_xy.x;
    float y = joint_2_point_xy.y;

    ChangePosPoint(joint_2_point, ControlJoint(), float2(x, y));
    ChangePosLine(joint_2_line, float2(x, y));

    // 각 변수 이름은 사진에 있는 이름을 참고
    // 그래픽을 표시할 땐 theta 2의 값을 구할 필요가 없음, 다만 나중에 로봇을 제어할 땐 필요함, C도 구할 필요는 없을 듯
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이
    float c = distance(center_point_xy, joint_2_point_xy); // 가상 라인의 길이, 유클리드 거리 사용, 첫번째와 마지막의 점의 길이를 구하는 것

    if (c <= joint_1_line_size + joint_2_line_size) { // 임시로 하는 것, c의 구하는 방식 때문에 제한없이 joint_2_point가 따라오고 그것과 센터 포인트의 길이를 구하면서 200을 넘어가게 되서.. 일단 임시로
        float C = CalSSSRadian(a, b, c, 3); // C의 각도를 구하는 것
        float B = CalSSSRadian(a, b, c, 2); // B의 각도를 구하는 것

        float I = CalSSSRadian(x - 250, y - 250, c, 2); // 250을 빼주는 이유는 계산했던 기준의 센터점이 0, 0 이였기 때문에 그쪽으로 옮기기 위해

        float theta1 = I - B;

        float j1x = a * cos(theta1) + 250; // 250을 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
        float j1y = a * sin(theta1) + 250;

        ChangePosLine(joint_2_line, float2(j1x, j1y), false);
        ChangePosPoint(joint_1_point, ControlJoint(), float2(j1x, j1y));
        ChangePosLine(joint_1_line, float2(j1x, j1y));

        char debug1[512];
        sprintf_s(debug1, "a = %.2f, b = %.2f, c = %.2f || C = %.2f, B = %.2f || I = %.2f, theta1 = %.2f", distance(center_point_xy, joint_1_point_xy), distance(joint_1_point_xy, joint_2_point_xy), c, C, B, I, theta1);
        debugText1().Text(to_hstring(debug1));

        char debug2[512];
        sprintf_s(debug2, "BX = %.2f, BY = %.2f || CX = %.2f, CY = %.2f || AX = %.2f, AY = %.2f", center_point_xy.x, center_point_xy.y, j1x, j1y, x, y);
        debugText2().Text(to_hstring(debug2));
    }
}

여기에서 UpdatePos 함수는 마우스의 위치 값을 가져오는 JointControlCanvasPointerMoved 함수에다가 넣었다. 

 

현재는 저기에서 모든 것을 구현하지만.. 나중에는 먼저 값을 계산해서 각 좌표 변수에다가 값을 넣고 UpdatePos를 실행해서 업데이트 할 예정이다.

 

여기에서 CalSSSRadian은 각도를 구하기 위한 함수이다. 여기에서 Radian은.. SSS로 계산을 하면.. Radian 값으로 나와서이다. 물론 이걸 각도로 다시 바꿀려면 radian * 180.0 / M_PI 이런식으로 하면 되긴 하는데.. 이후에 cos와 sin으로 밑변와 높이를 구할 때 각도를 넣는게 아닌 radian 값을 넣어서 구해야하기 때문이다.

 

그리고 if (c <= joint_1_line_size + joint_2_line_size) 이 if문이 필요한 이유는 c가 첫점과 끝점의 길이인데.. 각 팔의 길이가 100이고 더하면 200이다. 그래서 첫점과 끝점의 길이가 최대 200으로 밖에 될 수 밖에 없다.. 만약 여기에서 200 이상인걸 넣으면 cos나 sin 값이 계산이 안된다. 그렇기 때문에 미리 제한을 두는 것이다.

 

또한 아래에 debug라고 되어있는건 그냥 내가 실시간으로 각 값을 확인할려고 해둔 것이다.

그리고 250을 빼고 더하고 하는 부분이 있는데 이유는 내가 계산한 좌표는 첫점이 0, 0일 경우를 두고 한건데.. 지금의 경우 캔버스의 가로 세로가 500이기 때문에 가운데 좌표가 250, 250이다.. 그래서 이것 때문에 250, 250을 뺀 후에 계산을 하고 나중에 250, 250을 더하는 것이다. 물론 나중에 이것도 250, 250도 자동으로 구해서 자동으로 넣어지도록 할 예정이다. 암튼 이걸로 만든 프로그램을 실행해보았다..!!

 

결과

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

영상을 보면 알겠지만.. 망했다.. 중앙보다 아래에서 마우스를 움직일 경우에는 정상적으로 작동하지만.. 위에선 비정상적으로 작동하게 된다.. 물론 이 경우에 if문을 사용하면 처리가 가능하긴 하다. 각 4분면 마다 x, y 값을 경우데 따라서 반전 시키면 되긴 한다. 근데.. 굉장히 어렵기도 하고.. y값이 250 보다 작을 때는 다른 계산을 해서 해결하는 방법도 가능할 것이다.. 근데 해보긴 했다.. 근데 안된다.. 만약 이 방법이 되었다 해도 아마 쓰진 않았을 것 같다. 분명히 이건 나중에 로봇을 제어할 때 각도를 알아야 하기 때문이다.. 근데 4분면에서 필요에 따라 반전을 시키면 각도 값을 정확하게 알기는 어려울 것이다.. 그래서 이 방법은 버렸다..

 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

위에서 말한대로 하면.. 되긴 한다.. 근데 이것도.. 저렇게 되는거라면.. 문제가 있는 것이기 때문에.. 마찬가지로 버리기로 했다..

 

실패지만.. 실패는 아닌 듯..

사실 여기까지 구하기 위해.. 거의 잊어버린 수학을 다시 꺼내기 위해.. 삼각함수에 대해서 다시 공부해야하기도 했고.. 학교 수학 선생님이나.. 부모님이나.. IK와 로봇을 잘 아시는 분에게 물어보았다.. 결국에는 이 방법은 내가 찾았긴 했지만.. 이 것을 찾기 위해 도와주신 분들에게 뭔가.. 죄송한 마음이 들었다.. 물론 실패도 있어보는 것이.. 좋다고 할 순 있지만.. 그래도 도와주신 분들이 있는데.. 이렇게 실패를 해버리니.. 아쉬웠다..

 

어쨋든 이 방법은 실패를 했지만.. 그동안 내가 수학 수업을 받으면서.. 지루한 식 외우기나 문제집에서 내준 예시 문제를 푸는 것이 아닌.. 내가 직접 문제 상황을 해결하기 위해 수학을 쓸려는 예시를 보게 되어서.. 예전까지만 해도.. 코딩이나 로봇에는 + - * / 만 알면 되는줄 알았는데.. 그것이 아니란 것을 이제야 알 수 있었다.. 그래서 나는 이 과정을 통해서 엄청난 큰 도움을 얻기도 해서 실패라고 할 순 없는 것 같긴 하다..!

 

암튼 그래서 이번 파트는 여기까지 쓰지만.. 다음 파트에선 진짜로 IK를 구현한 것을 쓸 것이다..! 왜냐면.. 유튜브 영상에서 식을 가져왔기 때문.. 암튼.. 여기서 끝..! 내.. 3일..!! 어디갔어..!

반응형

+ Recent posts