[ Goal ] DirectXMath 라이브러리 이해


1. 벡터

  • 벡터는 크기와 방향을 값이다.
  • 같은 값을 가진 벡터라 해도 기준 좌표계가 서로 다르면 같은 벡터라 할 수 없다.
  • 벡터를 다룰 떄에는 주어진 벡터의 좌표가 현재 어떤 기준 좌표계에 상대적인지 알 필요가 있다.

 

  • 기본적인 벡터 연산들
    • 벡터 합 = 두 벡터의 합을 나타냄
    • 벡터 차 = 두 벡터의 차를 나타냄 ( 하나의 벡터가 다른 하나의 벡터로 이동하기위한 벡터를 구할때 사용 )
    • 스칼라 곱 = 벡터의 길이 증가, 감소
    • 벡터 내적 = 기하학적 의미로는 두 벡터가 이루는 각도를 구하기 위해 사용 
      • 내적을 이용해 벡터 projection이 가능하다. ( 그람-슈미트 직교화에서 사용 )
    • 벡터 외적 = 기하학적 의미로는 두벡터에 직교인 벡터를 생성할 때 사용

 

  • 직교화
    • 직교화는 주어진 벡터들로 부터 새로운 기준좌표계를 생성할때 주로 사용된다.
    • 1. 그람-슈미트 직교화 ( Gram-Schmidt Orthogonaliztion )는 내적을 이용
    • 2. 외적을 이용한 직교화 방법도 존재한다.
그람-슈미트 직교화 외적을 이용한 직교화

 


2. DirectXMath 라이브러리의 벡터

  • Windows SDK의 일부로써 SSE2명령 집합을 사용한다.
    • SSE2 = Streaming SIMD Extensions 2 ( 즉 SIMD를 사용한다. )
    • SIMD = 128비트 너비의 레지스터들을 이용해 32비트 float또는 int를 한꺼번에 처리 가능한 단위
  • SIMD을 이용하여 4차원 벡터의 연산을 빠르게 할수 있으며 만약 2차원 또는 3차원 벡터 계산시에는 SIMD의 성분에 0을 보내주어 연산을 할 수 있다.

 

2.1 사용 라이브러리

  1. DirectXMath.h = directXMath 라이브러리를 사용하기 위한 필수 라이브러리
  2. DirectXPackedVector.h = 몇가지 추가적인 자료 형식을 위한 라이브러리

 

2.2 벡터 형식들

  • DirectXMath 에서 핵심 벡터 형식은 SIMD 하드웨어 레지스터에 대응되는 XMVECTOR이다.
typedef __m128 XMVECTOR; 
  • 이때 __m128은 특별한 SIMD 형식이다. 벡터 계산시 SIMD의 장점이 발휘되려면 벡터가 반드시 이 형식이어야한다.

 

  • XMVECTOR는 16바이트 ( 128비트 )의 경계에 alignment 되어야 하는데, 지역 변수와 전역변수는 자동으로 이루어진다. 하지만 클래스 자료 멤버에는 이 형식 대신 XMFLOAT2, XMFLOAT3, XMFLOAT4 를 사용하는것을 권장한다.
// XMFLOAT4 구조체 예시
struct XMFLOAT4
{
    float x;
    float y;
    float z;
    float w;


 XMFLOAT4() {}
 XMFLOAT4(float _x, float_y, float_z, float _w) :
    x(_x), y(_y), z(_z), w(_w) {}
    
 explicit XMFLOAT4(_In_reads_ (4) const float *pArray) :
    x(pArray[0]), y(pArray[1]), z(pArray[2]), w(pArray[3]) {}
    
 XMFLOAT4& operator = (const XMFLOAT4 Float4)
 {
  x = Float4.x; y = Float4.y; z = Float4.z; w = Float4.w; return *this;
 }
};
  • 하지만 XMFLOATn과 같은 자료형식을 계산에 사용하면 SIMD의 장점을 취할 수 없다.
  • 이를 해결하기 위해 계산 전에 XMVECTOR로 변환하고 계산 후에는 다시 XMFLOATn으로 변환해야한다.
    • 적재 함수 = XMVECTOR로 변환
    •  
    • 저장 함수 = XMFLOATn으로 변환
// XM_CALLCONV는 레지스터 활용을 위한 호출 규약이 컴파일러마다 
// 다를수 있으므로 의존성을 없애기 위한 호출 규약 지시자이다.

// 적재함수
XMVECTOR XM_CALLCONV XMLoadFloat2(const XMFLOAT2 *pSource);
XMVECTOR XM_CALLCONV XMLoadFloat3(const XMFLOAT3 *pSource);
XMVECTOR XM_CALLCONV XMLoadFloat4(const XMFLOAT4 *pSource);


// 아래에 있는 FXMVECTOR 자료형에 대해서는 바로 아래에 설명

// 로드함수
void XM_CALLCONV XMStoreFloat2(XMFLOAT2 *pDestination, FXMVECTOR V);
void XM_CALLCONV XMStoreFloat3(XMFLOAT3 *pDestination, FXMVECTOR V);
void XM_CALLCONV XMStoreFloat4(XMFLOAT4 *pDestination, FXMVECTOR V);

 

 

정리하면 다음과 같다.

  1. 지역변수나 전역변수에는 XMVECTOR 사용
  2. 클래스 자료 멤버에는 XMFLOAT2, XMFLOAT3, XMFLOAT4를 사용
  3. 계산을 수행하기 전에 적재 함수들을 이용해 XMFLOATn을 XMVECTOR로 변환
  4. XMVECTOR 인스턴스들로 계산 수행
  5. 저장 함수들을 사용해 XMVECTOR를 XMFLOATn으로 변환

 

2.3 매개변수 전달

  • XMVECTOR 인스턴스를 인수로 해서 함수를 호출할 때, 효율성을 위해 XMVECTOR 값을 스택이 아닌 SSE/SSE2 레지스터를 통해 함수에 전달되어야 한다.
  • 이러한 방식으로 전달할수 있는 인수의 갯수는 플랫폼별로 상이하다.
    • 이를 해결하기 위해 XMVECTOR 매개변수에 대해 여러 형식을 사용한다.
    • 1. FXMVECTOR = 처음 세번째 XMVECTOR 매개변수까지 사용
    • 2. GXMVECTOR = 4번째 XMVECTOR 매개변수에 사용 
    • 3. HXMVECTOR = 5,6 번째 XMVECTOR 매개변수에 사용
    • 4. CXMVECTOR = 그 이후 XMVECTOR 매개변수에 사용
  • 또한 SSE/SSE2 이용한 호출 규약 역시 컴팡일러 마다 상이하다.
    • 이를 해결하기 위해 함수 이름 앞에 반듯 XM_CALLCONV 라는 호출규약 지시자를 붙인다.
// 매개변수가 모두 XMVECTOR로 이루어져있을 경우

inline XMMATRIX XM_CALLCONV XMMatrixTransformation(
    FXMVECTOR ScalingOrigin,
    FXMVECTOR ScalingOrientationQuaternion,
    FXMVECTOR Scaling,
    GXMVECTOR RotationOrigin,
    HXMVECTOR RotationQuaternion,
    HXMVECTOR Translation);
    
    
    
// 매개변수사이 XMVECTOR가 아닌 매개변수가 끼어있을 경우 XMVECTOR의 순서만 중요하다.

inline XMMATRIX XM_CALLCONV XMMatrixTransformation2D(
    FXMVECTOR ScalingOrigin,
    float     ScalingOrientation,
    FXMVECTOR Scaling,
    FXMVECTOR RotationOrigin,
    float     Rotation,
    GXMVECTOR Translation);

 

2.4 상수 벡터

  • 상수 XMVECTOR 인스턴스에는 반드시 아래의 매개변수를 사용해야한다.
    • Float32 = XMVECTORF32
    • Int32 = XMVECTORU32
static const XMVECTORF32 g_vHalfVector = { 0.5f, 0.5f, 0.5f, 0.5f};

static const XMVECTORU32 vGrabY = { 0x00000000, 0xFFFFFFFF, 0x00000000, 0x00000000 };

 

2.5 오버로딩 된 연산자들

  • 벡터간 연산 ( 덧셈, 뺄셈, 스칼라곱 ) 등을 위해 오버로딩된 연산자들 존재
XMVECTOR XM_CALLCONV operator+ (FXMVECTOR V);
XMVECTOR XM_CALLCONV operator+ (FXMVECTOR V);

XMVECTOR& XM_CALLCONV operator+= (XMVECTOR& V1, FXMVECTOR V2);
XMVECTOR& XM_CALLCONV operator-= (XMVECTOR& V1, FXMVECTOR V2);
XMVECTOR& XM_CALLCONV operator*= (XMVECTOR& V1, FXMVECTOR V2);
XMVECTOR& XM_CALLCONV operator/= (XMVECTOR& V1, FXMVECTOR V2);

XMVECTOR& operator*= (XMVECTOR& V, float S);
XMVECTOR& operator/= (XMVECTOR& V, float S);

XMVECTOR XM_CALLCONV operator+ (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR XM_CALLCONV operator- (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR XM_CALLCONV operator* (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR XM_CALLCONV operator/ (FXMVECTOR V1, FXMVECTOR V2);
XMVECTOR XM_CALLCONV operator* (FXMVECTOR V, float S);
XMVECTOR XM_CALLCONV operator* (float S, FXMVECTOR V);
XMVECTOR XM_CALLCONV operator/ (FXMVECTOR V, float S);

 

2.6 기타 상수 및 함수

  • 원주율이 포함된 여러 공식의 근삿값을 구하기 위해 아래와 같은 상수들이 정의되어있다.
const float XM_PI = 3.14592654f;
// 근사를 위한 저장되어있는 파이관련 상수들
const float XM_2PI = 6.283185307f;
const float XM_1DVPI = 0.318309886f;
const float XM_1DV2PI = 0.1591543f;
const float XM_PIDIV2 = 1.570796327f;
const float XM_PIDV4 = 0.785398163f;


// 아래와 같이 각도를 라디안 단위로 변환이 가능하다.
inline float XMConvertToRadians(float fDegrees) {

return fDegrees * (XM_PI / 180.0f);

}

inline float XMConvertToDegrees(float fRadians) {

return fRadians * (180.0f / XM_PI);

}

// 최소 최대값을 리턴하는 함수도 있다.
template<class T> inline T XMMin(T a, T b) { return (a < b) ? a : b; }
template<class T> inline T XMMax(T a, T b) { return (a < b) ? a : b; }

 

2.7 설정 함수

  • XMVECTOR 객체의 내용을 설정하는 용도로 아래와 같은 함수들이 존재
  • 즉 초기화 함수라 생각하면 된다.
// 0 Vector 반환
XMVECTOR XM_CALLCONV XMVectorZero();

// Vector (1, 1, 1, 1) 반환
XMVECTOR XM_CALLCONV XMVectorSplatOne();

// Vector (x, y, z, w) 반환
XMVECTOR XM_CALLCONV XMVectorSet(float x, float y, float z, float w);

// Vector (s, s, s, s) 반환
XMVECTOR XM_CALLCONV XMVectorReplicate(float Value);

// Vector (vx, vx, vx, vx) 반환
XMVECTOR XM_CALLCONV XMVectorSplatX(FMVECTOR V);

// Vector (vy, vy, vy, vy) 반환
XMVECTOR XM_CALLCONV XMVectorSplatY(FMVECTOR V);

// Vector (vz, vz, vz, vz) 반환
XMVECTOR XM_CALLCONV XMVectorSplatZ(FMVECTOR V);

 

2.8 벡터 함수들

  • 다양한 벡터 연산이 존재한다.
  • 함수 이름에 2, 3, 4가 붙어 있다면 각 차원의 벡터에 대한 연산이다.
  • 내적의 반환 형식이 XMVECTOR인 이유는 스칼라 연산과 SIMD 벡터 연산의 전환을 최소화 하기위함이다.
// 벡터의 길이 반환
XMVECTOR XM_CALLCONV XMVector3Length(FXMVECTOR V);

// 벡터의 길이 제곱 반환
XMVECTOR XM_CALLCONV XMVector3LengthSq(FXMVECTOR V);

// V1과 V2의 내적 반환
XMVECTOR XM_CALLCONV XMVector3Dot(FXMVECTOR V1, FXMVECTOR V2);

// V1과 V2의 외적 반환
XMVECTOR XM_CALLCONV XMVector3Cross(FXMVECTOR V1, FXMVECTOR V2);

// V 정규화
XMVECTOR XM_CALLCONV XMVector3Normalize(FXMVECTOR V);

// V에 수직인 벡터 반환
XMVECTOR XM_CALLCONV XmVector30rthgonal(FXMVECTOR V);

// V1과 V2 사이의 각도 반환
XMVECTOR XM_CALLCONV XMVector3AngleBetweenVectors(FXMVECTOR V1, FXMVECTOR V2);

void XM_CALLCONV XMVector3ComponentsFromNormal(
XMVECTOR* pParallel,       // proj_n(v) 반환 : n벡터를 기준으로 v를 영사
XMVECTOR* pPerpendicular,  // perp_n(v) 반환 : perp_n(v) = v - proj_n(v)
FXMVECTOR V,               // 입력 v
FXMVECTOR Normal);         // 입력 n

// V1 == V2 반환
bool XM_CALLCONV XMVector3Equal(FXMVECTOR V1, FXMVECTOR V2);

// V1 != V2 반환
bool XM_CALLCONV XMVECTOR3NotEqual(FXMVECTOR V1, FXMVECTOR V2);

 

 

2.9 부동소수점의 오차

  • 컴퓨터는 당연히 숫자를 정확하게 표현 할 수 없다.
  • 이러한 부정확함 떄는에 두 부동소수점 수의 상등을 판정할 때에는 두 수가 근사적으로 같은지를 본다.
  • 이때 사용되는 값은 입실론이다. ( 입실론 = 주관적인 아주 작은 수 )
#include <Windows.h> // XMVerifyCPUSupport에 필요함
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>

using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;


int main()
{
	cout.precision(8);

	// SSE2를 지원하는지 확인
	if (!XMVerifyCPUSupport)
	{
		cout << "DirectXMath를 지원하지 않음" << endl;
		return 0;
	}

	XMVECTOR u = XMVectorSet(1.0f, 1.0f, 1.0f, 0.0f);
	XMVECTOR n = XMVector3Normalize(u);

	float LU = XMVectorGetX(XMVector3Length(n));



	// 수학적으로는 길이가 반드시 1이어야 한다
	cout << LU << endl;

	if (LU == 1.0f)
	{
		cout << "길이 1" << endl;
	}
	else
	{
		cout << "길이 1이 아님" << endl;
	}

	// 1을 임의의 지수로 거듭제곱해도 여전히 1이어야 한다
	float powLU = powf(LU, 1.0e6f);
	cout << "LU^(10^6) = " << powLU << endl;

	return 0;

}

// 실행결과
// 0.9999994
// 길이 1 아님
// LU^(10^6) = 0.94213694
  • 위와 같이 부동소수점에는 오차가 존재한다 이를 해결하기 위해 아래와 같은 입실론을 사용한 코드가 존재한다.
XMFINLINE bool XM_CALLCONV XMVector3NearEqual(
    FXMVECTOR U,
    FXMVECTOR V,
    FXMVECTOR Epsilon
);

// 반환값 :
// abs(U.x-V.x) <= Epsilon.x &&
// abs(U.y-V.y) <= Epsilon.y &&
// abs(U.z-V.z) <= Epsilon.z

 


3. 예시 코드

#include <windows.h> // for XMVerifyCPUSupport
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;

// Overload the  "<<" operators so that we can use cout to 
// output XMVECTOR objects.
ostream& XM_CALLCONV operator << (ostream& os, FXMVECTOR v)
{
    XMFLOAT3 dest;
    XMStoreFloat3(&dest, v);

    os << "(" << dest.x << ", " << dest.y << ", " << dest.z << ")";
    return os;
}

int main()
{
    cout.setf(ios_base::boolalpha);

    // Check support for SSE2 (Pentium4, AMD K8, and above).
    if (!XMVerifyCPUSupport())
    {
        cout << "directx math not supported" << endl;
        return 0;
    }

    XMVECTOR n = XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f);
    XMVECTOR u = XMVectorSet(1.0f, 2.0f, 3.0f, 0.0f);
    XMVECTOR v = XMVectorSet(-2.0f, 1.0f, -3.0f, 0.0f);
    XMVECTOR w = XMVectorSet(0.707f, 0.707f, 0.0f, 0.0f);

    // Vector addition: XMVECTOR operator + 
    XMVECTOR a = u + v;

    // Vector subtraction: XMVECTOR operator - 
    XMVECTOR b = u - v;

    // Scalar multiplication: XMVECTOR operator * 
    XMVECTOR c = 10.0f*u;

    // ||u||
    XMVECTOR L = XMVector3Length(u);

    // d = u / ||u||
    XMVECTOR d = XMVector3Normalize(u);

    // s = u dot v
    XMVECTOR s = XMVector3Dot(u, v);

    // e = u x v
    XMVECTOR e = XMVector3Cross(u, v);

    // Find proj_n(w) and perp_n(w)
    XMVECTOR projW;
    XMVECTOR perpW;
    XMVector3ComponentsFromNormal(&projW, &perpW, w, n);

    // Does projW + perpW == w?
    bool equal = XMVector3Equal(projW + perpW, w) != 0;
    bool notEqual = XMVector3NotEqual(projW + perpW, w) != 0;

    // The angle between projW and perpW should be 90 degrees.
    XMVECTOR angleVec = XMVector3AngleBetweenVectors(projW, perpW);
    float angleRadians = XMVectorGetX(angleVec);
    float angleDegrees = XMConvertToDegrees(angleRadians);

    cout << "u                   = " << u << endl;
    cout << "v                   = " << v << endl;
    cout << "w                   = " << w << endl;
    cout << "n                   = " << n << endl;
    cout << "a = u + v           = " << a << endl;
    cout << "b = u - v           = " << b << endl;
    cout << "c = 10 * u          = " << c << endl;
    cout << "d = u / ||u||       = " << d << endl;
    cout << "e = u x v           = " << e << endl;
    cout << "L  = ||u||          = " << L << endl;
    cout << "s = u.v             = " << s << endl;
    cout << "projW               = " << projW << endl;
    cout << "perpW               = " << perpW << endl;
    cout << "projW + perpW == w  = " << equal << endl;
    cout << "projW + perpW != w  = " << notEqual << endl;
    cout << "angle               = " << angleDegrees << endl;

    return 0;
}

참고 : DirectX 12 를 이용한 3D 게임 프로그래밍 입문 / 한빛미디어

+ Recent posts