반응형

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

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

 

자 일단 내가 구상하고 있는건 두번째 끝점을 클릭한 상태로 드래그 하면 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일..!! 어디갔어..!

반응형
반응형

 

 

지식 없이 완전 처음부터 하니.. 틀린정보가 많습니다..

 

https://cheongpark.tistory.com/54

 

SCARA 로봇 제작하기 Pt. 6 (제작 - 끝)

이제.. 다 조립했으니.. 보드에 선을 연결해야하는데.. 제일 헷갈린 부분이 여기다.. 최대한 찾아서 끼우긴 했지만.. 계속 잘 못 끼웠었었다.. 그러다가 결국엔 끼웠지만.. 저 사진에 보이는 것 처

cheongpark.tistory.com

여기에서 로봇을 모두 제작했었다.

그런데 한가지 문제가 있다.. 로봇을 제어할 프로그램이 필요한데.. 예제에서 제공하는 프로그램은 도무지 알 수도 없고.. 여러 버그도 있고 잘 되지 않는 문제들이 있다.. 

그래서 그냥 처음부터 프로그램을 제작하는 것을 목표로 잡았다..

 

구상

일단 GUI 프로그램으로 만들 예정인데..

나에게는 웹페이지로 GUI를 구현하는 방법, 프로그램으로 GUI를 구현하는 방법 2가지가 있었는데..

난 여기에서 프로그램으로 GUI를 만드는걸로 선택했다.

이유는 프로그램으로 GUI를 만들어 본적이.. 예전에 OpenCV로 비슷하게나마 만들어본게 끝이고.. 제대로 만들어본 적은 없고.. C++를 연습하기 위해서이다.

일단.. C++로 프로그램을 제작한다면.. MFC나.. WinRT 이런게 있는데.. 나는 나중을 생각해서 크로스 플랫폼이 지원되는 WinRT로 정했다.

 

그래서 Visual Studio에 WinRT 프로그램을 제작하기 위한 설치 파일을 설치한 후 프로젝트를 만들었다.

 

그 후 일단 구상을 했는데

대충 이런 느낌으로 제작하는 것이다. 메인 그리드 부분에선 Inverse Kinematics로 로봇의 암을 제어하고 오른쪽 설정 창에선 집게를 펼지 말지 이런거를 세팅하고, 왼쪽에선 높이로 로봇의 Z축을 제어하는 곳으로 정했다.

또한 아래쪽의 검은색 저부분은 각각의 세팅 값이다. 설정 창에서 저장버튼을 눌르면 아래에 스택이 쌓이고 각 스택을 눌르면 그 때 저장했던 값들이 다시 나오게 되는 프로그램을 제작하기로 했다.

 

현재도 제작중이기 때문에.. 저기에서 어떤 점이 바뀔지는 모르겠지만.. 이 상태로 하기로 정했다.

 

GUI Xaml 제작 및 프로그래밍

일단 여기에서 제일 중요한 부분은 Inverse Kinematics 부분이다. 물론 각도로 일일이 다 지정을 할 순 있겠지만.. 그렇게 하면.. GUI 프로그램을 만드는 의미가 사라진다..

그래서 일단 저기를 구현해야하는데.. 구현하기 전에 사전에 세팅을 했다.

 

그리드 그리기

먼저 그리드를 표시하는 것!

일단 Xaml로 저것이 표시될 구간을 정한다!

현재 나의 경우엔.. 이걸 처음하다보니.. 아는게 없었다.. 그래서 어느정도 전문가가 보기엔 불편한 것들이 많겠지만.. 일단 이대로 진행하기로 했다. 아래 코드 구조에서 Canvas가 2개가 있는데.. 하나는 그리드를 표시하는 용이고 하나는 Inverse Kinematics로 팔 부분을 표시하는 것이다.

<Border
    Width="506"
    Height="506"
    BorderBrush="#f8f8f2"
    BorderThickness="3"
    CornerRadius="15,15,15,15"
    RelativePanel.AlignVerticalCenterWithPanel="True"
    Margin="200, 0, 0, 0">
    <!-- 그리드 위에 컨트롤를 넣는 느낌으로 할려 했지만 그리드가 나중에 렌더링 되어서 Canvas.ZIndex로 직접 바꿈 -->
    <Canvas
        x:Name="DrawGrid"
        Width="500"
        Height="500"
        Canvas.ZIndex="0"
        Background="Transparent"> 
        <Canvas
            x:Name="ControlJoint"
            Width="500"
            Height="500"
            Canvas.ZIndex="1"
            Background="Transparent"> <!-- 배경을 투명하게라도 주는 이유는 안하면 ControlJoint로 마우스 위치를 얻을려고 하면 Shape 부분만 얻을 수 있어서.. 전부 얻기 위해서 하는거 -->
        </Canvas>
    </Canvas>
</Border>

주석에 달린 것 처럼.. ZIndex를 넣은 이유가.. 그리드 배경 위에 팔 부분이 나오면 좋겠지만.. 렌더링 되는 것이.. 그리드가 먼저 되다보니.. ZIndex로 직접 바꾼거다.. 그리고 움직일 부분에서 마우스로 제어할 것인데.. 거기에서 마우스로 제어하는 경우 그리드에선 전부 추적이 가능하지만.. ControlJoint에선 팔 부분만 마우스의 좌표를 알 수 있다.. 그래서 그냥 배경을 추가하는 방식으로 진행했다.

 

이제 저렇게 하면 프로그램 안에 500 * 500 의 사각 박스가 나오게 된다.

이 사각 박스 안에 격자를 그려야하는데 이걸 위해 함수를 만들었다.

일단 line의 경우 좌표에서 필요한 속성은 X1, Y1, X2, Y2가 있다.

그래서 내가 생각한 방법으론 500을 내가 필요한 만큼의 라인 갯수로 나눠서 각 지점에 직접 배치하는 방식으로 했다.

 

그렇게 해서 C++ 코드를 작성했다.

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::DrawGridLines(int grid_x_count, int grid_y_count) {
    double grid_width = DrawGrid().ActualWidth();
    double grid_height = DrawGrid().ActualHeight();
    double line_thickness = 1;

    // 가로 줄 세로로 여러개 그리는거
    for (double i = grid_height / grid_y_count; i < grid_height; i += grid_height / grid_y_count) {
        Xaml::Shapes::Line line = Xaml::Shapes::Line();
        DrawLine(line, DrawGrid(), line_thickness, float2(0, i), float2(grid_width, i));
    }

    // 세로 줄 가로로 여러개 그리는거
    for (double i = grid_width / grid_x_count; i < grid_width; i += grid_width / grid_x_count) {
        Xaml::Shapes::Line line = Xaml::Shapes::Line();
        DrawLine(line, DrawGrid(), line_thickness, float2(i, 0), float2(i, grid_height));
    }
}

이 코드로 DrawGridLines를 처음에 프로그램을 시작할 때 실행해서 각 x축으로 몇개 y축으로 몇개 넣을지 선택해서 하면 

 

결과적으로 이렇게 표시가 된다..!

라인을 각각 10개씩 넣어서 했다.

 

마우스 위치 알아내고 표시하기

그리고 이후에 팔을 제어하기 위해 마우스를 사용해야해서 마우스 포인터의 위치를 알아야 한다..!

근데 일단 보통은 콘솔창에서 확인을 하는데.. 콘솔창은 어떻게 띄우는지도 모르겠고.. 그냥 GUI 화면에서 확인하는게 빠를 것 같아서 GUI 화면에서 표시되도록 했다..!

 

일단 Xaml 코드에선

<StackPanel
    BorderBrush="White"
    BorderThickness="2" RelativePanel.AlignBottomWithPanel="True" RelativePanel.AlignLeftWithPanel="True">
    <StackPanel
        Orientation="Horizontal">
        <TextBlock
            Width="50"
            TextAlignment="Center"
            Text="X"
            FontSize="35"/>
        <TextBlock
            x:Name="Point_X"
            Width="100"
            TextAlignment="Right"
            Text="0"
            FontSize="35"/>
    </StackPanel>
    <StackPanel
        Orientation="Horizontal">
        <TextBlock
            Width="50"
            TextAlignment="Center"
            Text="Y"
            FontSize="35"/>
        <TextBlock
            x:Name="Point_Y"
            Width="100"
            TextAlignment="Right"
            Text="0"
            FontSize="35"/>
    </StackPanel>
</StackPanel>

이런식으로 했다. X인지 Y인지 알 수 있게 하고 그 바로 옆에 숫자를 표시하도록 했다.

그래서 이걸로 해서 실행하면 저런식으로 나온다.

 

그리고 각 X, Y 좌표를 얻어야 한다.

이 경우엔 좀 간단한데.. 보통은 Xaml 코드에서 X, Y 값을 가져올 함수를 지정할 순 있지만.. 나는 나중에 함수 이름을 쉽게 변경하기 위해 모두 C++에서 진행했다.

일단 X, Y 좌표를 가져올 캔버스는 ControlJoint라는 것이기 때문에 여기의 PointerMoved로 접근해서 함수로 넘겨주는 방식으로 했다.

void MainPage::Init() {
	ControlJoint().PointerMoved({ this, &MainPage::JointControlCanvasPointerMoved });
}

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));
}

이런식으로 하면 마우스 포인터의 값을 가져와서 아까 만들었던 Text로 집어넣게 된다.

Init 함수는 그냥 임의로 만든거고.. 프로그램이 시작할 때 실행되도록 하면 된다. 그렇게 해서 마우스 값을 넘겨주고 거기에서 X, Y 값을 뽑아서 Text를 지정하는 방식이다.

 

결과물!!

그래서 이것으로 일단 프로젝트 준비는 끝났다..!!

이걸로 지금까지 한 것의 결과물을 보면 아래 처럼된다..!

그렇게 해서 일단 Pt. 1은 끝났다..! 

이 모든 것이.. 3일 정도 걸려서 알아내고 만든 결과다..! 물론 GPT의 도움도 받긴 했지만.. 암튼.. 다음에선 IK를 구현하는 것이다..!

반응형

+ Recent posts