In chapter 1 of 3D Game Programming with DirectX 12 by Frank Luna, there is a brief overview of some vector basics, followed by an overview of the DirectXMath.h math library. This uses the “SSE2” instruction set (Streaming SIMD Extensions 2), which introduces double-precision floating point instructions. What’s important to know here is that 128-bit wide SIMD (single instruction multiple data) instructions can do things to four 32-bit floats or “ints` with one instruction.
Naturally, this is terrific for vector calculations; for example, we can perform vector addition with one SIMD instruction instead of four scalar instructions. Note that we can just ignore coordinates if our vectors have less than 4 dimensions.
Before anything else, I want to stress that what I describe below is not comprehensive, and are effectively just notes that I’m taking. If you want a thorough description of DirectX concepts, you should probably look elsewhere.
What does this look like in code? In summary:
-
The core vector type in DirectX Math is
XMVECTOR. -
XMVECTORmust be 16-byte aligned, so for class data members, we useXMFLOAT2,XMFLOAT3,XMFLOAT4. -
To make use of SIMD, we use loading functions to convert from
XMFLOATntoXMVECTORwhenever we need to do calculations. -
As parameters, we can pass
XMVECTORvalues in SSE2 registers instead of on the stack. Because of differences in platforms and compilers, passing vectors looks like this:- The first three
XMVECTORparameters should beFXMVECTOR - The fourth should be
GXMVECTOR - The fifth and sixth:
HXMVECTOR - Any further parameters:
CXMVECTOR
- The first three
-
Use
XM_CALLCONVto use the proper calling convention. -
For constructors, it’s
FXMVECTORfor the first three, andCXMVECTORfor the rest. Constructors don’t use theXM_CALLCONVannotation. -
Constant
XMVECTORinstances should useXMVECTORF32, orXMVECTORU32for integer data.
It is not yet explained clearly why the parameter passing is so strange, so hopefully it’s covered eventually. The book then details setter functions and vector functions, which I won’t list out here. However - it is interesting to note that functions that should usually produce scalar output (such as the dot product) actually still return XMVECTORs, preventing mixing up scalar and SIMD vector operations for efficiency.
Here’s a short program that demonstrates some of the functions for vector math.
#include <windows.h>
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iomanip>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;
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
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.0, 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 math
XMVECTOR a = u + v;
XMVECTOR b = u - v;
XMVECTOR c = 10.0f * u;
// ||u||
XMVECTOR L = XMVector3Length(u);
// u/||u||
XMVECTOR d = XMVector3Normalize(u);
// u dot v
XMVECTOR s = XMVector3Dot(u, v);
// u x v
XMVECTOR e = XMVector3Cross(u, v);
XMVECTOR projW;
XMVECTOR perpW;
XMVector3ComponentsFromNormal(&projW, &perpW, w, n);
bool equal = XMVector3Equal(projW + perpW, w) != 0;
bool notEqual = XMVector3NotEqual(projW + perpW, w) != 0;
XMVECTOR angleVec = XMVector3AngleBetweenVectors(projW, perpW);
float angleRadians = XMVectorGetX(angleVec);
float angleDegrees = XMConvertToDegrees(angleRadians);
cout << left << setfill(' ') << setw(20) << "u" << right << setfill(' ') << setw(20) << "= " << u << endl;
cout << left << setfill(' ') << setw(20) << "v" << right << setfill(' ') << setw(20) << "= " << v << endl;
cout << left << setfill(' ') << setw(20) << "w" << right << setfill(' ') << setw(20) << "= " << w << endl;
cout << left << setfill(' ') << setw(20) << "n" << right << setfill(' ') << setw(20) << "= " << n << endl;
cout << left << setfill(' ') << setw(20) << "a = u + v" << right << setfill(' ') << setw(20) << "= " << a << endl;
cout << left << setfill(' ') << setw(20) << "b = u - v" << right << setfill(' ') << setw(20) << "= " << b << endl;
cout << left << setfill(' ') << setw(20) << "c = 10 * u" << right << setfill(' ') << setw(20) << "= " << c << endl;
cout << left << setfill(' ') << setw(20) << "d = u / ||u||" << right << setfill(' ') << setw(20) << "= " << d << endl;
cout << left << setfill(' ') << setw(20) << "e = u x v" << right << setfill(' ') << setw(20) << "= " << e << endl;
cout << left << setfill(' ') << setw(20) << "L = ||u||" << right << setfill(' ') << setw(20) << "= " << L << endl;
cout << left << setfill(' ') << setw(20) << "s = u.v" << right << setfill(' ') << setw(20) << "= " << s << endl;
cout << left << setfill(' ') << setw(20) << "projW" << right << setfill(' ') << setw(20) << "= " << projW << endl;
cout << left << setfill(' ') << setw(20) << "perpW" << right << setfill(' ') << setw(20) << "= " << perpW << endl;
cout << left << setfill(' ') << setw(20) << "projW + perpW == w" << right << setfill(' ') << setw(20) << "= " << equal << endl;
cout << left << setfill(' ') << setw(20) << "projW + perpW != w" << right << setfill(' ') << setw(20) << "= " << notEqual << endl;
cout << left << setfill(' ') << setw(20) << "angle" << right << setfill(' ') << setw(20) << "= " << angleDegrees << endl;
return 0;
}
And the output:
u = (1, 2, 3)
v = (-2, 1, -3)
w = (0.707, 0.707, 0)
n = (1, 0, 0)
a = u + v = (-1, 3, 0)
b = u - v = (3, 1, 6)
c = 10 * u = (10, 20, 30)
d = u / ||u|| = (0.267261, 0.534522, 0.801784)
e = u x v = (-9, -3, 5)
L = ||u|| = (3.74166, 3.74166, 3.74166)
s = u.v = (-9, -9, -9)
projW = (0.707, 0, 0)
perpW = (0, 0.707, 0)
projW + perpW == w = true
projW + perpW != w = false
angle = 90
We also have the XMMATRIX type with its own conventions. For class data members, we use XMFLOAT4X4. When passing a matrix as a parameter, one matrix counts as four XMVECTOR parameters. So, the first XMMATRIX is of type FXMMATRIX, and subsequent parameters are of type CXMMATRIX. DirectX Math also recommends using CXMMATRIX for constructors that takes XMMATRIX parameters. Sample program below:
#include <windows.h>
#include <DirectXMath.h>
#include <DirectXPackedVector.h>
#include <iostream>
using namespace std;
using namespace DirectX;
using namespace DirectX::PackedVector;
// overload "<<"
ostream& XM_CALLCONV operator<<(ostream& os, FXMVECTOR v)
{
XMFLOAT4 dest;
XMStoreFloat4(&dest, v);
os << "(" << dest.x << ", " << dest.y << ", " << dest.z << ", " << dest.w << ")";
return os;
}
ostream& XM_CALLCONV operator<<(ostream& os, FXMMATRIX m)
{
for ( int x = 0; x < 4; ++x )
{
os << XMVectorGetX(m.r[x]) << "\t";
os << XMVectorGetY(m.r[x]) << "\t";
os << XMVectorGetZ(m.r[x]) << "\t";
os << XMVectorGetW(m.r[x]) << "\t";
os << endl;
}
return os;
}
int main()
{
if (!XMVerifyCPUSupport())
{
cout << "directx math not supported" << endl;
return 0;
}
XMMATRIX A(1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 2.0f, 0.0f, 0.0f,
0.0f, 0.0f, 4.0f, 0.0f,
1.0f, 2.0f, 3.0f, 1.0f);
XMMATRIX B = XMMatrixIdentity();
XMMATRIX C = A * B;
XMMATRIX D = XMMatrixTranspose(A);
XMVECTOR det = XMMatrixDeterminant(A);
XMMATRIX E = XMMatrixInverse(&det, A);
XMMATRIX F = A * E;
cout << "A = " << endl << A << endl;
cout << "B = " << endl << B << endl;
cout << "C = A*B =" << endl << C << endl;
cout << "D = transpose(A) = " << endl << D << endl;
cout << "det = determinant(A) = " << det << endl << endl;
cout << "E = inverse(A)" << endl << E << endl;
cout << "F = A*E" << endl << F << endl;
}
And the output:
A =
1 0 0 0
0 2 0 0
0 0 4 0
1 2 3 1
B =
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
C = A*B =
1 0 0 0
0 2 0 0
0 0 4 0
1 2 3 1
D = transpose(A) =
1 0 0 1
0 2 0 2
0 0 4 3
0 0 0 1
det = determinant(A) = (8, 8, 8, 8)
E = inverse(A)
1 0 0 0
0 0.5 0 0
0 0 0.25 0
-1 -1 -0.75 1
F = A*E
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1