Unity Study

2025-12-02_Quaternion(3)

NyumMa 2025. 12. 2. 20:00

Quaternion(3)

쿼터니언은 unityengine.coremodule.dll unityengine.quaternion으로 타입이 선언되어 있으며

각 구조체의 메서드, 연산자 오버로딩, 연산처리 및 return 타입 등이 선언되어 있음

 

Angle = rad * 57.29578f (180/π)

각도와 라디안 표기법에 대한 결과값 변환식

 

Epsilon = 1E-06f;

앱실론은 수학적으로 매우 작은 값을 의미하며, 컴퓨터에서 0으로 나누게 되면 IEEE-754 부동소수점 표기법에서 부호, 지수, 가수로 나누어 데이터를 저장할 때

지수부의 표현이 최대가 되어(8비트 255)

결과적으로 메모리에서는 무한대, 의미적으로서는 유용하지 않은 값을 나타냄.

따라서 해당 값이 처리되는 경우 NaN(not a number)을 반환하도록 되어 있음

 

따라서 시스템, 및 엔진에서는 해당 값의 처리를 앱실론을 통해 매우 작은 값으로 대처하거나

조건 분기로 해당 값을 나누어 처리함

 

주로 벡터 정규화 식에서 매우 작은 값이 들어왔을 때 벡터의 크기가 매우 작으면 이러한 문제가 발생하므로 해당 값을 앱실론을 통하여 매우 작은 값으로 나누어 처리함

혹은 deltatime 등의 프레임 혹은 물리 연산에서 0이 되는 것을 방지하기 위해서 앱실론을 사용하기도 함

 

 

Quaternion.FromToRotation(from, to)

from 벡터를 to 벡터 방향으로 회전하기 위한 쿼터니언을 생성함
from
에서 to로 회전하는 최단거리의 회전을 위해서 fromto에 수직하는 성분 계산인 외적계산을 사용함

그리고 나서 fromto에 대한 각도를 내적 연산을 통해서 구함

해당 회전축과 해당 각도만큼 회전하기 위해 쿼터니언을 생성하고 이를 적용하여 from -> to 로 하는 쿼터니언을 생성함

// 매개 변수:
   //   fromDirection:
   //     A non-unit or unit vector representing a direction axis to rotate.
   //
   //   toDirection:
   //     A non-unit or unit vector representing the target direction axis.
   //
   // 반환 값:
   //     A unit quaternion which rotates from fromDirection to toDirection.

 

LookAt

여기서는 lookat과 같은 transform의 타입은 native c++에서 계산이 처리되므로

(marshalnotnull 부분에서 c#에서 쓰인 값 타입을 참조하고 c++ 네이티브의 포인터 객체를 가리켜 계산함)

, 내부적으로 들어다 보기는 어렵지만, 문서로 보았을 때 월드 공간을 기준으로 한 transfrom임을 확인할 수 있음.

LookAt을 사용할때는 부모자식의 관계를 조심하게 보아야 함

자식에서 LookAt 사용시 부모의 TRS 관계에 의해서 자식의 rotation 처리 등이 잘못되기에 이러한 경우에는 LookAt 메서드를 사용하기 보다 직접적으로 해당 방향에 대한 회전을 생성해 주는 것이 좋다 -> lookrotation을 사용하자

public void LookAt(Vector3 worldPosition)
{
    Internal_LookAt(worldPosition, Vector3.up);
}

[FreeFunction("Internal_LookAt", HasExplicitThis = true)]
private void Internal_LookAt(Vector3 worldPosition, Vector3 worldUp)
{
    IntPtr intPtr = MarshalledUnityObject.MarshalNotNull(this);
    if (intPtr == (IntPtr)0)
    {
        ThrowHelper.ThrowNullReferenceException(this);
    }
    Internal_LookAt_Injected(intPtr, ref worldPosition, ref worldUp);
}

 

Quaternion.LookRotation (target, axis)

target방향으로 axis를 축으로 한 회전을 생성

일반적으로 axis는 로컬, 혹은 월드 좌표의 up vector를 사용하는 경우가 많음

부모 자식인 관계일때는 LookAt을 자식에서 사용시 기본적으로 월드 공간에 대한 회전이 적용되기에

직접적으로 target을 바라보도록 하는 Quaternion을 생성하여 부모의 local 공간에대 적용해야 한다. 

 

Quaternion.ToAngleAxis(out float angle, out Vector3 axis)

쿼터니언을 축(벡터)와 회전 각도(angle)로 변환

각도는 실수부가 wcos세타/2임을 통해 arccos으로 계산

여기서 angle 값은 rad이므로 180/pi로 각도로 변환

축도 vsin(세타/2)를 통해 계산, 여기서 sin(세타/2) 가 되는데

여기서 회전각이 0이 되는 경우에는 0으로 나눠지기 때문에

epsilon 값 체크하여 기본값으로 사용함.

c#에서 out을 통해 변수에 값을 반환할 수 있도록 함

 

Quaternion.Inverse(q)

q conjugate, 의미적으로는 축이 반대 방향이 되므로 축에 대하여 반대 방향에 대한 회전을 의미함

q.inverse 는 q(-x,-y-,z,w)을 의미함

 

Quaternion.identity

허수부가 0이고 실수부가 1인 쿼터니언(회전이 없는 쿼터니언)

x y z w의 단위 길이가 1이 되도록

 

Quaternion.Angle(q1, q2)

두 쿼터니언의 각도 차이를 계산, q1 -> q2를 위한 최소 각도를 반환

여기서 최소 각도는 q1 q2를 외적으로 하는 축을 회전축으로 했을때의 angle 값이 됨

 

Quaternion.Dot(q1, q2)

두 쿼터니언의 유사도 수치를 내적 연산을 통해 판단함

일반적인 vec4 내적으로 처리

x1x2 + y1y2 + z1z2 + w1w2

여기서 나온 수치 등은 Slerp 연산에서 보간시에 사용됨

Slerp시 구면보간하는 방향은 크게 2개로써 큰각과 작은각으로 나눠지는데 유사도에 따라 각도를 선택하기도 함

public static float Dot(Quaternion a, Quaternion b)
    {
        return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
    }

 

Quaternion.Normalize(q)

쿼터니언 연산시 수학적으로 norm2의 크기가1인 쿼터니언을 사용해야 올바른 회전을 표현할 수 있음

유니티에서 생성된 쿼터니언 등은 대부분 정규화 되어 있지만, 쿼터니언을 이용해 연속적으로 값을 곱하거나 할 시에 부동소수점 오차로 인해 정규화 값이 1이 안될수도 있음

혹은 쿼터니언을 직접 생성하는 경우나

선형보간시(lerp) 해당 값이 정규화 되어 있지 않기에

다음과 같은 경우 등에서 사용하고, 내부적으로 쿼터니언은 정규화 된 값으로 결과가 반환됨

public static Quaternion Normalize(Quaternion q)
{
    float num = Mathf.Sqrt(Dot(q, q));
    if (num < Mathf.Epsilon)
    {
        return identity;
    }
    return new Quaternion(q.x / num, q.y / num, q.z / num, q.w / num);
}

 

 

Quaternion.ToString()

쿼터니언 값을 읽을 수 있는 문자열 형태로 반환함

순서는 x,y,z,w로 표시

쿼터니언은 기본적으로 해당 사원수가 어떤 회전을 의미하는지 판단하기 어려우므로 디버깅 용도나 기록 등에 사용함

 

Quaternion.Lerp(q1, q2, t)

Linear interpolation

q1, q2t, (1-t)로 각각 가중치를 두어 보간함

q = q1 * t + q2 * (1-t)

여기서 q값은 정규화 되어 있지 않으므로 추가로 q 정규화 처리를 해주어야 함

또한 모든 보간은 보간에 대한 큰각, 작은각 두가지로 나뉘어져 있기 때문에 두 쿼터니언을 dot product을 통해 start end중 하나를 부호 반전하여 회전각을 선택할 수 있음

 

Quaternion.LerpUnclamped(q1, q2, t)

Lerp와 동일하지만 여기서 t 값에 대해서 이전에는 가중치의 합이 1이 되도록 해야 하면

해당 메서드는 t 값에 제한이 없음 ->

회전을 q1 q2 범위를 넘어서 확장할 때, 과도한 카메라 회전 등의 처리 등에 사용됨.

단 두 가중치의 합은 1을 유지해야함

 

Quaternion.Slerp(q1, q2, t)

Lerp는 선형 보간이기에 애니메이션 처리시 각속도가 일정하지 못하여 애니메이션에서 중간에 속도 변화 등이 나타난다

이를 보간하기 위한 방법으로 각을 사용하여 보간하는 방식인 slerp(spherical linear interpolation)을 사용함

q1, q2 dot을 통해 이 수치를 slerp의 보간 인자로 사용한다.

Sin(1-t)세타, sint세타로 하여 구면 보간, 원호에 비례한 보간으로 sin세타를 분모에 나누어 결과값이 극과 극일 때 결과 값이 q1, q2가 되도록 한다.

 

Quaternion.SlerpUnclamped(q1, q2, t)

Lerpunclamped와 마찬가지로 t의 범위를 확장하여 표현

 

Quaternion.RotateTowards(from, to, maxangle)

From쿼터니언에서 to 쿼터니언의 회전을 최대 maxangle만큼 회전시킬 때 사용함


 

 

q * v

 public static Vector3 operator *(Quaternion rotation, Vector3 point)
 {
     float num = rotation.x * 2f;
     float num2 = rotation.y * 2f;
     float num3 = rotation.z * 2f;
     float num4 = rotation.x * num;
     float num5 = rotation.y * num2;
     float num6 = rotation.z * num3;
     float num7 = rotation.x * num2;
     float num8 = rotation.x * num3;
     float num9 = rotation.y * num3;
     float num10 = rotation.w * num;
     float num11 = rotation.w * num2;
     float num12 = rotation.w * num3;
     Vector3 result = default(Vector3);
     result.x = (1f - (num5 + num6)) * point.x + (num7 - num12) * point.y + (num8 + num11) * point.z;
     result.y = (num7 + num12) * point.x + (1f - (num4 + num6)) * point.y + (num9 - num10) * point.z;
     result.z = (num8 - num11) * point.x + (num9 + num10) * point.y + (1f - (num4 + num5)) * point.z;
     return result;
 }

 

쿼터니언 생성시 각의 절반각인 seta/2  를 사용하여

q*vq*^-1연산을 수행하지만 , q와 v를 계산하고 이를 다시 q^-1를 계산하는것은 계산 처리에 비용이 크기 때문에 이를 한번에 계산하기 위해서

q를 3x3 회전행렬로 변환 후 벡터에 곱해주도록 되어 있다.

 

R을 벡터 V와 곱해주어 R 축에 대해 선형변환하여 각 오브젝트의 x y z에 대한 월드공간 좌표를 반환한다. 

리턴 타입은 vector3 타입

 

(오브젝트는 기본적으로 여러 vertex의 조합이므로 오브젝트를 형성하는 vertex 집합들이 모두 선형변환되어 결과적으로 회전된 모습을 보인다.)

 

q1 * q2

[MethodImpl(MethodImplOptions.AggressiveInlining)]
 public static Quaternion operator *(Quaternion lhs, Quaternion rhs)
 {
     return new Quaternion(
     lhs.w * rhs.x + lhs.x * rhs.w + lhs.y * rhs.z - lhs.z * rhs.y, 
     lhs.w * rhs.y + lhs.y * rhs.w + lhs.z * rhs.x - lhs.x * rhs.z, 
     lhs.w * rhs.z + lhs.z * rhs.w + lhs.x * rhs.y - lhs.y * rhs.x, 
     lhs.w * rhs.w - lhs.x * rhs.x - lhs.y * rhs.y - lhs.z * rhs.z);
 }

 q1q2의 결과는

각각을

q1 = (x1,y1,z1,w1)

q2 = (x2,y2,z2,w2) 라 했을 때

 

q = q1 * q2

 

q.x = w1x1 + x1w2 + y1z2 - z1y2

q.y = w1y2 + y1w2 + z1x2 - x1z2

q.z = w1z2 + z1w2 + x1y2 - y1x2

q.w = w1w2 - x1x2 - y1y2 - z1z2

 

가 됨을 직접 16개의 항으로 전개하여 정리할 수 있다.

 

IsEqualUsingDot, ==, !=

이밖에는 쿼터니언간의 비교에 대한 함수와 연산자 오버로딩이다. 

특징적으로는 dot 연산에서 equal시

앱실론을 통해 오차 범위를 고려하여 처리한다는 점

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsEqualUsingDot(float dot)
{
    return dot > 0.999999f;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(Quaternion lhs, Quaternion rhs)
{
    return IsEqualUsingDot(Dot(lhs, rhs));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Quaternion lhs, Quaternion rhs)
{
    return !(lhs == rhs);

 

다음은 

  • Obj1(object), Obj2(target)가 있을 때
  • Obj1(object)이 Obj2를 바라보면서(target)
  • obj2의 로컬 공간에 obj2.up 방향을 축으로
  • 직선거리로 5만큼 차이를 두며
  • Obj2를 바라보면서
  • 공전하는 기능을 넣고 싶다.

에 대한 구현임

 

먼저 회전에 대한 부분은 기본적으로 LateUpdate에 구현함

movement와 같은 부분이 update가 진행된 후에

rotation처리를 해 주어야 도중 튀거나 하는 현상을 막을수 있음

 

1. Hierarchy 창에 obj를 target의 자식으로 두어 target의 TRS에 대응되로록 한다. 

2. angle값을 매 프레임마다 갱신되도록 하며 일정한 각속도로 유지하기 위해 프레임간의 시간차인 deltatime을 곱한다.

3. 변환은 local좌표를 기준으로 변환되도록 해야 하므로 localposition 메서드를 사용한다.

4. position은 local공간 기준으로의 부모(0,0,0)으로부터 distance만큼 떨어져 있는 위치에서 회전하도록 한다.

-> 오브젝트는 vertex의 집합이며

쉽게 해석해서 distance만큼 떨어진 한점(실제로는 집합)이

localrot 쿼터니언을 통해 (회전축과 angle) 정해진 회전축과 각도로

회전한 결과의 위치를 update마다 재계산하는것,

여기서는 아직 회전하지 않은 상태이므로 이 식만 보면 오브젝트의 forward는 변하지 않고 앞을 바라보고 있는 상태이다.

5. 오브젝트와 타겟의 방향 벡터는 target - obj 를 뺀 벡터를 정규화

6. 쿼터니언 회전을 로컬회전을 기준으로 하여 적용함으로써 obj가 target을 바라보도록 하는 회전을 매 obj의 로컬 포지션마다 생성하여 적용한다.

 

 

다시 강조할 부분은

엔진 처리 순서는

어짜피 local공간에서 먼저 position 및 회전 처리가 된 다음에

world공간으로 배치가 되는 순서임

void LateUpdate()
{
    angle += rotatespeed * Time.deltaTime;
    
    Quaternion localRot = Quaternion.AngleAxis(angle, Vector3.up);

    Vector3 localoffset = new Vector3(0, 0, -distance);
    obj.localPosition = localRot * localOffset;

    Vector3 dir = (obj.localPosition - target.localPosition).normalized;
    Quaternion look = Quaternion.LookRotation(dir, Vector3.up);

    obj.localRotation = look;
     

 

'Unity Study' 카테고리의 다른 글

2025-12-08_TPS System Basic  (0) 2025.12.08
2025-12-05_Script Cycle + GC  (0) 2025.12.05
2025-12-01_Parent + Child Hierarchy TRS  (0) 2025.12.01
2025-11-28 Quaternion(2)  (0) 2025.11.28
2025-11-27 Quaternion  (0) 2025.11.27