[게임 수학] 오일러 각과 외적

ounols·2021년 11월 11일
3

게임 수학

목록 보기
2/9
post-thumbnail
post-custom-banner

🧐 해당 파트는 게임 개발 환경을 구성하는 컴퓨터 그래픽스(Computer Graphics)를 이해하기 위한 기초 수학의 간단한 개념에 대해 설명하고 있습니다!

혹여나 이해가 잘 안되거나 잘못된 정보를 발견하시게 되었다면 관련해서 피드백 해주시면 정말 감사하겠습니다!

1. 오일러 각

지난번 1편에서 봤던 오일러 각은 회전 행렬에다가 표준기저벡터를 하나씩 다 대입해서 넣는 불상사를 막아준 정말 최고의 친구였습니다.

이렇게 편리하고 좋은 개념인 오일러 각만 사용하면 회전 문제는 모두 끝날까요?
사실 문제점이 몇가지 존재합니다. 그 중 대표적인 문제점 두가지를 다뤄보고자 합니다.

1-1. 오일러 각에서 발생하는 짐벌락 현상이란?

안타깝게도 짐벌락이라는 문제점이 발생해버립니다! 이 짐벌락이 어떻게 발생하는지 간단한 영상과 함께 알아보도록 합시다.

https://youtu.be/zc8b2Jo7mno?t=123

짐벌락에 대해 쉽게 이해할 수 있는 유튜브 영상

제 주변에서 짐벌락이 대체 뭐냐? 라고 질문하는 친구들이 있으면 이런 영상을 항상 보여줍니다!
영상이 정말 깔끔하게 설명되어 있어서 개인적으론 이런 영상을 정말 좋아하는 편입니다.

이제 저 영상에서 눈여겨 봐야하는 부분이 바로 순차적으로 회전을 시킨다 입니다.
이전에서 1편에서 다뤘듯이 회전 행렬은 각 축마다 서로 호환되지 않기 때문에 축마다 곱하는 식으로 회전 행렬을 얻어왔습니다.

위 로직에 따라 오일러 각도도 각 축마다 하나씩 이동을 하다보니 다음에 곱해지는 축들에게 무조건적으로 간섭이 일어납니다.

그럼 오일러 각을 쓰는 이상 짐벌락은 해결 절대 못하나요?
일단 지금의 대중화된 기술력으로는 없는 것으로 알고 있습니다...하핫...
그래도 사원수라는 친구가 이 곳에 작성된 오일러 각의 문제점을 해결해줍니다!

게임 수학 시리즈의 후반부에 사원수도 다루니 꼭 확인해주시길 바랍니다!

짐벌락에 대한 일화


사실 아폴로 프로젝트를 진행하면서 짐벌락 현상으로 애를 많이 먹었다는 일화가 있습니다. 근데 아폴로 프로젝트 뿐만이 아니였습니다....!


한창 냉전 중이였던 시기의 소련의 달탐사를 위한 로켓인 N1역시 짐벌락 현상으로 인해 로켓이 분해되어 폭발해버린 기록이 있습니다....
(이미지 및 일화의 출처 : 나무위키 및 sturman님 블로그)

1-2. 오일러 각의 값을 그대로 보간하는 경우 엉뚱한 회전이 나오나요?

자 다음과 같은 오일러 각 2개가 있습니다.

A=Euler(10,0,0)B=Euler(30,0,0)A = Euler(10, 0, 0) \\ B = Euler(30, 0, 0)

이 두 각의 중간값은 어떻게 될까? 과연 (15,0,0)(15, 0, 0)이 될까?
이를 알아보기 위해선 회전의 보간을 이용하면 됩니다.

회전의 보간에서 Ra,RbR_a, R_b두 각을 통해 끝점을 알아내는 방식은 두 각의 회전을 곱하면 끝점인 결과값이 나오는 것이 위 그림의 전제입니다.

따라서 먼저 두 각의 회전 행렬을 적용합니다.

Ra=RyawaRpitchaRrollaRb=RyawbRpitchbRrollbR_a = R_{yaw_a} \cdot R_{pitch_a} \cdot R_{roll_a} \\ R_b = R_{yaw_b} \cdot R_{pitch_b} \cdot R_{roll_b}

여기서 두 각은 yawyaw값만 존재하기 때문에 아래와 같이 정리할 수 있습니다.

Ra=RyawaIIRb=RyawbIIR_a = R_{yaw_a} \cdot I \cdot I \\ R_b = R_{yaw_b} \cdot I \cdot I

이제 식을 풀어보겠습니다!

RbRa=(RyawaII)(RyawbII)=RyawbRyawa=Ryaw(b+a)R_b \cdot R_a = (R_{yaw_a} \cdot I \cdot I) \cdot (R_{yaw_b} \cdot I \cdot I) \\ = R_{yaw_b} \cdot R_{yaw_a} \\ = R_{yaw_{(b+a)}}

오! RyawR_{yaw}값만 남았고 이 값에 들어가는 각도가 서로 덧셈을 이루고 있습니다.

이 내용을 다시 말하면 RaR_aRbR_b의 각도를 더하면 최종적인 각을 얻을 수 있고, 이를 통해 오일러 각 AABB의 중간값은 (15,0,0)(15, 0, 0)로 볼 수 있습니다!

저는 여기서 뭔가 찜찜해집니다. 회전의 보간은 한 평면에서 진행되는 것 처럼 보이는데 그럼 다른 축이 들어가면 저희가 알고있는 평면이 아닌거 같은 느낌이 들기 시작해졌습니다.

A=Euler(10,10,0)B=Euler(30,30,0)A = Euler(10, 10, 0) \\ B = Euler(30, 30, 0)

그렇다면 이번엔 위와 같은 오일러 각의 전제를 두고
위 두 각에 맞게 pitchpitch축을 더해서 과연 중간값이 (15,15,0)(15, 15, 0)이 되는지 다시 전개를 해보겠습니다.

Ra=RyawaRpitchaIRb=RyawbRpitchbIRbRa=(RyawbRpitchbI)(RyawaRpitchaI)=(RyawbRpitchb)(RyawaRpitcha)R_a = R_{yaw_a} \cdot R_{pitch_a} \cdot I \\ R_b = R_{yaw_b} \cdot R_{pitch_b} \cdot I \\ \\ R_b \cdot R_a = (R_{yaw_b} \cdot R_{pitch_b} \cdot I) \cdot (R_{yaw_a} \cdot R_{pitch_a} \cdot I) \\ = (R_{yaw_b} \cdot R_{pitch_b}) \cdot (R_{yaw_a} \cdot R_{pitch_a})

앗 여기서 더이상 진행하기 힘듭니다. 이런 식이라면 앞서 봤던 각도만 더하는 식으로만 해결하기엔 문제가 있어보입니다.

이러한 문제 때문에 (15,15,0)(15, 15, 0)이 중간값이 된다고 볼 수 없겠군요... 우리가 예상하던 평면이 아닌 행렬을 통해 계산된 공간에서 보간을 진행해야하기 때문에 쉽게 결과값을 나타내긴 힘들어보입니다.

1-3. 그럼 어떻게 해야 이런 문제가 해결되나요?

우리는 지금까지 오일러 각의 문제점들을 알아봤는데 이를 어떻게 해결할 수 있을까요?
이런 문제를 해결 할 수 있는 2가지 대표적인 방식이 존재합니다. 당장은 다루지 않지만 미리 알려드리자면 아래와 같습니다.

  • 로드리게스 회전
  • 사원수

저희는 위의 2가지를 통해의 오일러 각의 문제점을 한번 해결해보도록 하겠습니다!
일단 위의 내용 중 로드리게스 회전을 이해하기 전에 외적에 대해 먼저 알아보도록 하겠습니다.

2. 벡터 외적

2-1. 벡터 외적의 수식과 특징

벡터 외적은 두 벡터를 통해 계산이 이루어지며 다음과 같은 수식으로 나타낼 수 있습니다.

u=(ux, uy, uz), v=(vx, vy, vz)u×v=(uyvzuzvy, uzvxuxvz, uxvyuyvx)\vec u= (u_x,\ u_y,\ u_z),\ \vec v =(v_x,\ v_y,\ v_z) \\ \vec u \times\vec v =(u_yv_z-u_zv_y,\ u_zv_x-u_xv_z,\ u_xv_y-u_yv_x)

💡 이전에 작성했던 한 축에 대한 회전행렬을 회전에 따라 곱한 방식을 사용했을 때 xyzx \to y \to z 순서로 차례대로 계산했던 기억이 있으신가요?

이 외적도 마찬가지로 xx빼고 순서대로 yzyz, yy빼고 순서대로 zxzx, zz빼고 순서대로 xyxy 이런 느낌으로 값이 들어간다고 생각하면 외우기 편리합니다!

이렇게 수식을 외우는 팁을 보니 생각보다 간단한 수식이였습니다!
이러한 벡터 외적의 특징은 다음과 같습니다.

  • 외적을 할 수 있는 최소한의 차원은 3차원이다.
    외적은 3n3n차원으로 표현할 수 있다고 합니다! 비록 3차원이지만 2D를 위한 비슷한 개념이 따로 있다고 하네요!

  • 외적의 결과 값은 벡터값이다.
    스칼라 값이 나오는 내적과 달리 외적은 벡터값을 뱉어냅니다.

  • 분배법칙은 성립하지만 결합법칙과 교환법칙은 불가능하다.
    특이하게도 교환법칙은 불가능 하지만 교환 법칙을 적용했을 때 서로의 결과값은 반대로 나옵니다! 너무 신기해!

이렇게 보니 외적은 좀 특이한 친구군요! 나중에 내적과 함께 비교하면 정말 신기하게 서로 창과 방패처럼 나타나는데 이 점은 아래에서 다루도록 하겠습니다!

일단 외적의 큰 특징들을 알아봤으니 이번엔 성질에 대해 알아보겠습니다!

😮 [추가 설명] 외적의 성질

  • 같은 벡터를 서로 외적하면 영벡터가 된다!
    이 부분은 위에 알아봤던 수식을 통해 이야기를 하면 이해가 됩니다.
    일단 xx값을 보자면 uyvzuzvyu_yv_z-u_zv_y 인데
    사실상 서로의 2개의 포지션을 곱하고 빼주는 것이기 때문에 0이 나올 수 밖에 없습니다.

    이처럼 다른 값도 모두 0으로 나와 결국 영백터가 된다는 것을 증명해주고 있습니다.

    물론 서로 반대되는 벡터를 외적해도 위와 같은 원리로 영벡터가 나옵니다!

  • 스칼라 값을 각 벡터에 넣어도 영벡터가 된다!
    사실 이 부분이 평행성을 판별하는 부분입니다! 이 부분은 2-2. 벡터 외적의 평행성 판별에서 다루도록 하겠습니다!

  • v×u\vec v \times \vec uv×u\vec v \times \vec {u_\bot}과 같다! (v\vec vu\vec u를 외적한다면 v\vec vu\vec u의 직교벡터를 외적하는 것과 같다)
    u\vec u는 표준기저벡터의 성질에 의해 u+u\vec{u_\parallel}+\vec{u_\bot}로 표현할 수 있습니다.
    이를 식으로 표현한다면 다음과 같습니다.

    v×u=v×(u+u)=v×u+v×u\vec{v}\times\vec{u}=\vec{v}\times\left(\vec{u_\bot}+\vec{u_\parallel}\right)=\vec{v}\times\vec{u_\bot}+\vec{v}\times\vec{u_\parallel}

    앗! 여기서 v×u\vec{v}\times\vec{u_\parallel}는 서로 평행하는 성질로 인해 결국 영백터가 됩니다!
    따라서 v×u=v×u\vec{v}\times\vec{u}=\vec{v}\times\vec{u_\bot}가 성립하게 되는 것입니다.

    혹시 이러한 성질로 각도를 통해 외적을 구할 수 있나요?
    물론입니다! 삼각함수를 이용하여 각도를 통해 구하는 것이 가능합니다.
    위 그림과 같이 sin\sin의 성질로 인해 y=rsinθy = r \cdot \sin\theta 라는 식이 성립되고
    이를 u=usinθ|\vec {u_\bot}| = |\vec u| \cdot \sin\theta 로 볼 수 있겠습니다.

    💡 이전에 배웠던 내적은 cos\cos으로 각도를 통해 계산을 했는데 이번 외적은 sin\sin으로 계산합니다. 뭔가 서로 밀접한 관계가 있음을 조금씩 느껴지기 시작합니다!

2-2. 벡터 외적의 평행성 판별

위에서 봤듯이 같은 벡터를 외적하면 영벡터가 된다는 사실을 알았습니다.
그렇다면 같은 벡터가 아닌 서로 평행한 벡터를 외적하면 어떻게 될까요? 또 평행하다는 것의 판별은 어떻게 해야할까요?

이번엔 한 벡터에 스칼라 kk를 곱하고 외적을 해보겠습니다.

u×ku=(kuyuzkuzuy, kuzuxkuxuz, kuxuykuyux)=(0,0,0)\vec u \times k\vec u = (ku_yu_z-ku_zu_y,\ ku_zu_x-ku_xu_z,\ ku_xu_y-ku_yu_x) \\ = (0, 0, 0)

외적을 하니 평행하는 두 벡터도 영벡터가 나오게 됩니다.
따라서 서로 평행할 때 외적시키면 무조건 영벡터가 나오는데 이걸 가지고 평행성을 판별할 수 있겠습니다.

2-3. 벡터 외적의 크기는 항상 두 벡터 사잇각의 사인함수와 비례함을 정리해봅시다.

혹시 두 벡터의 크기는 같지만 서로 각도가 조금씩 다르면 외적의 크기값도 달라진다는 사실을 알고 계신가요?
위에 정리했던 내용을 토대로 직접 값을 넣어서 확인하면 방향은 같은데 크기가 조금씩 다른 신기한 결과를 알 수 있습니다!

위의 사진을 보시면 θ\theta값에 따라 직교하는 u\vec {u_\bot}의 크기가 다른 것을 볼 수 있습니다.
이는 위에서 증명했던 u=usinθ|\vec {u_\bot}| = |\vec u| \cdot \sin\theta 식에서 sin\sin의 성질에 따라

sinθ=uu\sin\theta = {|\vec {u_\bot}| \over |\vec u|} 즉 직교벡터인 외적의 크기는 θ\theta값에 비례한 것을 알 수 있습니다.

💡 놀랍게도 u\vec uv\vec v와 방향이 같아지면 직교 벡터가 사라지는 것을 볼 수 있습니다!
이렇게 두 벡터의 방향이 같아지면 외적값이 영벡터가 되는 성질을 다시 알 수 있었습니다.

2-4. 왜 벡터 외적 결과는 언제나 평면의 방향을 나타내는 노멀 벡터가 되나요?

노멀벡터는 평면의 방향을 나타내는 친구로써 그래픽스에서 빛과 관련한 모든 계산으로부터 꼭 필요한 필수요소입니다. 물론 실사용에선 매쉬에 저장된 노멀값을 사용하지만 탄젠트 노멀을 사용할 때엔 외적이 사용됩니다!

그럼 이제 노멀벡터가 평면으로부터 무조건 직교해야하는 성질임을 알고 이를 외적의 식으로 나타내봅시다.
여기서 저는 외적 결과값을 내적하여 내적의 값이 0이면 직교하는 성질을 이용해 알아보도록 하겠습니다. 이걸 식으로 정리해보겠습니다.

u(u×v)=uxuyvzuxvyuz+uyuzvxuyvzux+uzuxvyuzvxuy=0v(u×v)=vxuyvzvxvyuz+vyuzvxvyvzux+vzuxvyvzvxuy=0\vec{u}\cdot\left(\vec{u}\times\vec{v}\right)=u_xu_yv_z-u_xv_yu_z+u_yu_zv_x-u_yv_zu_x+u_zu_xv_y-u_zv_xu_y=\vec{0} \\ \vec{v}\cdot\left(\vec{u}\times\vec{v}\right)=v_xu_yv_z-v_xv_yu_z+v_yu_zv_x-v_yv_zu_x+v_zu_xv_y-v_zv_xu_y=\vec{0}

오 신기합니다! 식은 좀 복잡해보이지만 차근차근 풀다보면 영벡터가 나오는 모습을 볼 수 있습니다!
이렇게 두 벡터의 외적 결과는 항상 두 벡터에게 직교하는 점을 알아봤습니다.

근데 외적을 반대로 하면 다른 값이 나오는데 이것도 직교하나요?

외적의 특징 중 하나가 바로 '교환법칙은 성립하지 않지만 반대 방향의 벡터가 나온다' 입니다.
이것만 놓고 봐도 반대 방향의 벡터도 결국 두 벡터와 직교하는 형태이니 이를 내적하면 영벡터가 나올 것임이 틀림없습니다

사실 하나씩 풀어서 쓰더라도 외적은 반대방향으로 나오지만 내적하면 같은 결과로 나옵니다!

3. 벡터 외적의 활용

3-1. 벡터 외적을 활용한 좌우 판별 방법을 알아보고, 이를 구현한 간단한 예제를 제작해봅시다.

상하를 판별하는 방법을 내적으로 표현했다면, 좌우를 판별할 땐 외적을 사용합니다.
와 이번에도 소름돋게 서로 또 밀접한 관계가 있는 것 같습니다!

어쨌든 이러한 외적을 이용해 좌우를 구별하는 간단한 예제를 만들어봅시다!

먼저 이론은 이렇습니다!

저희는 킹갓제너럴앙페르의오른손나사의법칙 을 이용하여 다음과 같이 쉽게 직교하는 방향을 유추할 수 있습니다. 고마워요! 앙페르!

고등학교에서 잠깐 배웠던 자기장 법칙을 이렇게도 쓰다니 진짜 엄청난 우연인 것 같습니다.

아무튼 다시 본론으로 돌아가자면 아래와 같은 이미지로 요약이 됩니다.

앞서 설명드렸던 앙페르의 오른손 나사의 법칙에 의해 fv\vec f → \vec v의 방향과 fv\vec f → \vec {v'}와 서로 반대되는 방향으로 외적이 됨을 유추해볼 수 있습니다.
이렇게 얻어온 외적값을 이 좌표계의 Up벡터와 함께 내적을 한다면 위와 아래를 1,11, -1로 구분할 수 있게 됩니다.

💡 내적은 두 벡터가 서로 같은 방향을 이룬다면 11, 반대 방향이면 1-1로 나타납니다.

예제 결과

저는 제가 만든 엔진을 통해 직접 예제를 제작하고 실행하였습니다! 로그에 나타나는 결과 값은 카메라 시선 벡터 기준으로 오브젝트가 어느 방향에 있는지를 판별합니다.

예제 코드 리뷰

아래는 Scene에 임의로 배치하는 C++ 코드입니다.

아래는 오브젝트에 컴포넌트로 들어간 Wave.nut이란 Squirrel Script 파일입니다.

class Wave extends CSEngineScript {
    center_object = null;
    y_up = null;

    function Init() {
        // 센터 오브젝트를 받아옵니다
        center_object = gameobject.Find("center object");
        // up벡터를 생성합니다. (자체 제작 엔진은 y-up입니다.)
        y_up = vec3();
        y_up.Set(0, 1, 0);
    }

    function Tick(elapsedTime) {
        local move_value = vec3();
        move_value.Set(sin(elapsedTime * 0.001), 0, -3);

        // 양옆으로 왔다갔다하는 vec3를 포지션값으로 넣습니다.
        GetTransform().position = move_value;

        // 로그를 출력합니다.
        LoggingResults();
    }

    function LoggingResults() {
        // 시선 벡터와 오브젝트의 벡터를 받아옵니다
        local target_vec = GetTransform().position;
        local object_vec = center_object.GetTransform().position;

        // 외적 후 Up벡터를 이용해 내적을 진행합니다.
        local cross_result = target_vec.Cross(object_vec);
        local dot_result = cross_result.Dot(y_up);

        // 결과값을 출력합니다.
        Log("[Result] 오브젝트는 " + 
		(dot_result > 0 ? "왼쪽에 있습니다." : "오른쪽에 있습니다."));
    }
}

데모 씬의 초기 세팅 부분에 오브젝트를 세팅하는 코드를 넣었고
Tick 함수는 커스텀 스크립트를 통해 진행하였습니다.

엔진은 현재 한창 디퍼드 렌더링을 구현하는 중이라 이 예제의 코드를 깃허브 같은 곳에 올리지 못하는 점 양해 부탁드립니다...ㅠㅜ

🎇 자체 엔진이 궁금하다면 CSEngine 프로젝트 깃허브 링크에서 확인 가능합니다!

3-2. 벡터 외적을 활용해 시선 벡터로부터 회전 행렬을 만드는 과정을 정리합시다.

static Matrix4<T> LookAt(const Vector3<T>& eye, const Vector3<T>& target, const Vector3<T>& up) {

	Vector3<T> z = (eye - target).Normalized();
	Vector3<T> x = up.Cross(z).Normalized();
	Vector3<T> y = z.Cross(x).Normalized();

	Matrix4<T> m;

	m.MAT4_XX = x.x; m.MAT4_XY = x.y; m.MAT4_XZ = x.z; m.MAT4_XW = 0;
	m.MAT4_YX = y.x; m.MAT4_YY = y.y; m.MAT4_YZ = y.z; m.MAT4_YW = 0;
	m.MAT4_ZX = z.x; m.MAT4_ZY = z.y; m.MAT4_ZZ = z.z; m.MAT4_ZW = 0;
	m.MAT4_WX = 0; m.MAT4_WY = 0; m.MAT4_WZ = 0; m.MAT4_WW = 1;
    
	[...]
    
	return m;
}

위의 제가 오래전에 제작한 코드와 함께 설명하도록 하겠습니다!
순서는 zxyz → x → y순서로 로컬 값을 얻어내고, 행렬에 해당 로컬값들을 집어넣습니다.

  1. zz 로컬에 시선벡터를 노멀화해서 넣습니다.
  2. xx 로컬은 upup벡터와 z\vec z의 외적을 구하여 두 벡터를 직교하는 벡터를 넣습니다.
    혹시 모르니 노멀화 해서 넣습니다.
  3. yy 로컬은 서로 직교하는 z\vec zx\vec x를 외적하여 두 벡터를 직교하는 벡터를 넣습니다.
    이론 상 노멀화를 안해도 되지만 부동 소수점이기 때문에 노멀화를 꼭 해줍시다.
  4. 이제 모든 로컬값을 구했으니 행렬에 쏙쏙 넣어줍니다.

💡 안타깝게도 아직 자체엔진에선 카메라가 위에서 내려다보고 있을 경우를 대비한 예외처리를 하지 못했습니다! 일단 할일 목록에 추가는 해놨고 나중에 예외처리가 진행될 예정입니다.

그렇다면 위에서 바라볼 때의 예외처리는 어떻게 하나요?
위에서 아래로 수직으로 내려본다면 외적에 사용했던 upup벡터와 평행을 이루어 영벡터가 나타나기 때문에 더 이상 진행이 힘듭니다. 그렇다면 어떻게 해결해야할까요?

사실 상 z\vec z는 공간 좌표계의 x,zx, z축과 수직인 성질을 가지고 있습니다. 따라서 upup벡터 대신 축인 (1,0,0)(1, 0, 0) 또는 (0,0,1)(0, 0, 1)을 넣어주면 됩니다!

이는 두 벡터가 서로 평행하지 않고 그 중 한 벡터가 월드 좌표계와 평행 또는 수직인 벡터라면 외적의 결과값도 크게 다르지 않기 때문에 가능한 일이였습니다.

4. 벡터 내적과 외적의 정리

지금까지 진행하면서 내적과 외적의 밀접한 관계가 있는 것 같은 떡밥을 드디어 풀 때가 왔습니다!

특징외적내적
결과값벡터스칼라
계산 가능한 최소 차원3차원2차원
θ\theta를 이용하여 식을 전개할 때sin\sin을 사용cos\cos을 사용
θ\theta에 따른 음수/양수 영역
두 벡터가 평행할 때영벡터11 또는 1-1
두 벡터가 직교할 때두 벡터와 다른 로컬축의 직교하는 벡터00
결과값이 00 또는 영벡터라면?두 벡터가 서로 평행함두 벡터가 서로 직교함
두 벡터를 통한 판별법평행성 판별(좌우)직교성 판별(상하)
교환법칙서로 반대되는 벡터성립
결합법칙성립하지 않음성립하지 않음
분배법칙성립성립

뭔가 서로 티격태격하면서도 서로 보완해주는 느낌이 정말 강합니다.

앞으로도 내적과 외적을 통해 많은 계산을 할 것이기에 이렇게 정리를 통해 한번 다시 짚고 넘어가는 것이 좋을 것 같습니다.

profile
(게임 엔진 프로그래머가 되고싶은) 게임 클라이언트 프로그래머
post-custom-banner

0개의 댓글