[ 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 사용 라이브러리
- DirectXMath.h = directXMath 라이브러리를 사용하기 위한 필수 라이브러리
- 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);
정리하면 다음과 같다.
- 지역변수나 전역변수에는 XMVECTOR 사용
- 클래스 자료 멤버에는 XMFLOAT2, XMFLOAT3, XMFLOAT4를 사용
- 계산을 수행하기 전에 적재 함수들을 이용해 XMFLOATn을 XMVECTOR로 변환
- XMVECTOR 인스턴스들로 계산 수행
- 저장 함수들을 사용해 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 게임 프로그래밍 입문 / 한빛미디어
'독학 > DirectX' 카테고리의 다른 글
[ DirectX ] Direct3D 기초 4. Direct3D의 초기화 (1) (0) | 2022.11.11 |
---|---|
[ DirectX ] 기초 수학 3. 변환 (0) | 2022.11.09 |
[ DirectX ] 기초 수학 2. 행렬 대수 (1) | 2022.11.09 |