48 SimpleMath
Chuck Walbourn редактировал(а) эту страницу 2024-10-09 21:50:55 -07:00
DirectXTK DirectXTK12

SimpleMath.h wraps the DirectXMath SIMD vector/matrix math API with an easier to use C++ interface. It provides the following types, with similar names, methods, and operator overloads to the XNA Game Studio math API:

Related tutorials: Using the SimpleMath library, Basic game math, Collision detection, Mixing SimpleMath and DirectXMath

classDiagram
class XMFLOAT2{
    +x
    +y
}
class Vector2
XMFLOAT2 <|-- Vector2
class XMFLOAT3{
    +x
    +y
    +z
}
class Vector3
XMFLOAT3 <|-- Vector3
class XMFLOAT4{
    +x
    +y
    +z
    +w
}
class Vector4
XMFLOAT4 <|-- Vector4
class Plane
XMFLOAT4 <|-- Plane
class Quaternion
XMFLOAT4 <|-- Quaternion
class Color
XMFLOAT4 <|-- Color
class XMFLOAT4X4{
    +m[4][4]
}
class Matrix
XMFLOAT4X4 <|-- Matrix
classDiagram
class Rectangle{
    +x
    +y
    +width
    +height
}
class Viewport{
    +x
    +y
    +width
    +height
    +minDepth
    +maxDepth
}
class Ray{
    +position
    +direction
}

Header

#include <d3d11.h>
#include "SimpleMath.h"
#include <d3d12.h>
#include "SimpleMath.h"
#include "SimpleMath.h"
// This does not have interop for Direct3D 11 or Direct3D 12 types

Namespace

All the functions in SimpleMath are in the DirectX::SimpleMath C++ namespace.

using namespace DirectX::SimpleMath;

C++ operator overloads will only work if the operator is in the current namespace scope. A similar issue happens with the DirectXMath XMVECTOR and XMMATRIX operator overloads if you don't have using namespace DirectX;.

Why wrap DirectXMath?

DirectXMath provides highly optimized vector and matrix math functions, which take advantage of SSE SIMD intrinsics when compiled for x86/x64, or the ARM NEON instruction set when compiled for an ARM platform such as Windows RT or Windows Phone. The downside of being designed for efficient SIMD usage is that DirectXMath can be somewhat complicated to work with. Developers must be aware of correct type usage (understanding the difference between SIMD register types such as XMVECTOR vs. memory storage types such as XMFLOAT4), must take care to maintain correct alignment for SIMD heap allocations, and must carefully structure their code to avoid accessing individual components from a SIMD register. This complexity is necessary for optimal SIMD performance, but sometimes you just want to get stuff working without so much hassle!

Enter SimpleMath...

These types derive from the equivalent DirectXMath memory storage types (for instance Vector3 is derived from XMFLOAT3), so they can be stored in arbitrary locations without worrying about SIMD alignment, and individual components can be accessed without bothering to call SIMD accessor functions. But unlike XMFLOAT3, the Vector3 type defines a rich set of methods and overloaded operators, so it can be directly manipulated without having to first load its value into an XMVECTOR. Vector3 also defines an operator for automatic conversion to XMVECTOR, so it can be passed directly to methods that were written to use the lower level DirectXMath types.

If that sounds horribly confusing, the short version is that the SimpleMath types pretty much "Just Work" the way you would expect them to.

By now you must be wondering, where is the catch? And of course there is one. SimpleMath hides the complexities of SIMD programming by automatically converting back and forth between memory and SIMD register types, which tends to generate additional load and store instructions. This can add significant overhead compared to the lower level DirectXMath approach, where SIMD loads and stores are under explicit control of the programmer.

You should use SimpleMath if you are:

  • Looking for a C++ math library with similar API to the C# XNA types
  • Porting existing XNA code from C# to C++
  • Wanting to optimize for programmer efficiency (simplicity, readability, development speed) at the expense of runtime efficiency

You should go straight to the underlying DirectXMath API if you:

  • Want to create the fastest possible code
  • Enjoy the lateral thinking needed to express algorithms in raw SIMD

This need not be a global either/or decision. The SimpleMath types know how to convert themselves to and from the corresponding DirectXMath types, so it is easy to mix and match. You can use SimpleMath for the parts of your program where readability and development time matter most, then drop down to DirectXMath for performance hotspots where runtime efficiency is more important.

Collision types

The SimpleMath wrapper does not include classes for bounding volumes because they are already implemented in the DirectXMath library, and are included by the SimpleMath.h header.

This includes:

These were designed to already be similar to the XNA Game Studio math API bounding types.

Note: The BoundingFrustum class's CreateFromMatrix function assumed Left-Handed coordinates. In DirectXMath 3.16 or later, there's a defaulted parameter you can use for Right-Handed projection matrices: BoundingFrustum::CreateFromMatrix(fr, matrix, true);

Coordinate Systems

Because SimpleMath Is intended to look a lot like like XNA Game Studio's math library, it uses right-handed coordinates by convention for the following Matrix methods:

  • CreatePerspectiveFieldOfView
  • CreatePerspective
  • CreatePerspectiveOffCenter
  • CreateOrthographic
  • CreateOrthographicOffCenter
  • CreateLookAt

If you want to use left-handed coordinates, use the DirectXMath methods directly. For example

// Here's a RH example
Vector3 eye, target, up;
...
Matrix mView = Matrix::CreateLookAt( eye, target, up );

// Here' is a LH example of same thing which relies on
// Vector3 and Matrix conversion operators
Vector3 eye, target, up;
...
Matrix mView = XMMatrixLookAtLH( eye, target, up );
SimpleMath RH DirectXMath LH
CreatePerspectiveFieldOfView XMMatrixPerspectiveFovLH
CreatePerspective XMMatrixPerspectiveLH
CreatePerspectiveOffCenter XMMatrixPerspectiveOffCenterLH
CreateOrthographic XMMatrixOrthographicLH
CreateOrthographicOffCenter XMMatrixOrthographicOffCenterLH
CreateLookAt XMMatrixLookAtLH

With the methods Matrix::CreateBillboard and Matrix::CreateConstrainedBillboard the handedness is inherent in the default vectors for cameraForward, and objectForward which are right-handed. You can make them behave left-handed by providing appropriate vectors.

Vector3 objectPosition, cameraPosition, rotateAxis;

Matrix mView = Matrix::CreateBillboard( objectPosition,
    cameraPosition, Vector3::Up, Vector3::Backward );

Matrix mView = Matrix::CreateConstrainedBillboard( objectPosition,
    cameraPosition, rotateAxis, Vector3::Backward, Vector3::Backward );

Both DirectXMath and SimpleMath use row-major matrices, row vectors, and pre-multiplication.

Using with HLSL

SimpleMath as with DirectXMath uses row-major ordering for matrices. This means that elements are stored in memory in the following order:_11, _12, _13, _14, _21, _22, etc.

HLSL as noted on Microsoft Docs defaults to using column-major ordering as this makes for slightly more efficient shader matrix multiplies. Therefore, if a Matrix is going to be copied into a HLSL constant buffer, it is usually transposed to flip the ordering to _11, _21, _31, _41_, _12, _22, etc. as part of updating the constant buffer.

With the built-in Effects this is done internally, but if writing your own shaders and managing your own constant buffers, you will need to ensure you pass in your matrix data in the correct order for your HLSL shader settings. This means sticking with the HLSL default by transposing your matrices as you update the constant buffer, using #pragma pack_matrix(row_major) in your HLSL shader source, or compiling your shaders with D3DCOMPILE_PACK_MATRIX_ROW_MAJOR / /Zpr.

Windows Store apps

A number of Windows Store samples include a BasicMath.h header that includes some very simplistic math types:

  • Vector2
  • Vector3
  • Vector4
  • Matrix4x4

It is fairly easy to use SimpleMath in place of BasicMath, but keep in mind that the BasicMath.h header types are actually templates so they are typically used as "Vector2". SimpleMath types are always single-precision float numbers so do not make use of template syntax.

BasicMath.h also includes typedef aliases for these types to make them use the same naming as HLSL vector types, although the usage syntax and operations are quite different:

  • float2
  • float3
  • float4
  • float4x4

If you like the HLSL-like type names, you could use SimpleMath in place of BasicMath.h by adding a type alias:

using float2 = DirectX::SimpleMath::Vector2;
using float3 = DirectX::SimpleMath::Vector3;
using float4 = DirectX::SimpleMath::Vector4;
using float4x4 = DirectX::SimpleMath::Matrix;

Note: The actual operations would look different than the way they would using BasicMath.

XMVECTOR and XMMATRIX

The DirectXMath library is intended as a light-weight wrapper over the underlying SIMD instructions supported by modern CPUs. SIMD stands for 'Single-Instruction-Multiple-Data' and is a specific form of parallelism. The idea is pretty straight-forward: the registers X and Y contain more than one element (commonly 4 32-bit floating point values) in each. You then do an instruction, such as X * Y, and the output is more than one result (commonly 4 values from X are multiplied with 4 values from Y resulting in 4 values in the result).

DirectXMath is by design a 'leaky abstraction' in that it exposes the architectural restrictions to the developer. The 'work-horse' types that essentially every DirectXMath function takes as input parameters and produces as results is either XMVECTOR (4 32-bit floating-point values) or XMMATRIX (a 4x4 matrix of 32-bit floating-point values). These types have a strict requirement of being aligned to 16-byte boundaries in memory. If this restriction is not met, the program will crash with a mis-alignment exception at runtime.

Using XMVECTOR or XMMATRIX as local variables on the stack or as a global or static variable in memory will automatically respect the alignment requirements, but this is not necessarily true of memory allocated from the 'heap' (i.e. new for C++ programs or malloc for C programs). It happens that x64 native (64-bit) applications use 16-byte alignment for the heap by default, but this is not true of x86 (32-bit) or ARM applications. As such, using XMVECTOR or XMMATRIX directly in a struct or class allocated on the heap can be dangerous or at least less trivial than it first appears. There are ways to ensure that heap-allocated memory is always aligned, but generally it's more trouble than it's really worth in practice. There are also subtle ways that C++ constructs can misalign structures that you thought were properly aligned.

As such, DirectXMath includes a number of memory analogs for XMVECTOR and XMMATRIX, and explicit load & store operations for moving the data into and out of them. This is actually quite efficient, particularly if you are able to move them from memory into an XMVECTOR or XMMATRIX, then perform a long chain of operations on that data before storing the result back to memory.

One of SimpleMath's primary functions is to hide the existence of XMVECTOR and XMMATRIX. Instead, you can use Vector3 or Matrix without any alignment requirements, and the C++ compiler will automatically call the required load and store functions. This makes working with DirectXMath much more forgiving, at the potential cost of extra load and stores taking place 'invisible' to the programmer.

Since this is implemented using C++ conversion operators, you can at any point use a SimpleMath type directly with a DirectXMath function that takes XMVECTOR and/or XMMATRIX. They are also completely equivalent to a standard memory type in the DirectXMath library:

  • Vector2 <-> XMFLOAT2, XMVECTOR
  • Vector3 <-> XMFLOAT3, XMVECTOR
  • Vector4 <-> XMFLOAT4, XMVECTOR
  • Quaternion <-> XMFLOAT4, XMVECTOR
  • Color <-> XMFLOAT4, XMVECTOR
  • Plane <-> XMFLOAT4, XMVECTOR
  • Matrix <-> XMFLOAT4X4, XMMATRIX

You can therefore freely mix SimpleMath types with DirectXMath functions at any point.

C++20 Support

With the May 2022 release of the DirectX Tool Kit, C++20 spaceship operator (<=>) is supported for Rectangle and Viewport. This requires building with /std:c++20 /Zc:_cplusplus.

The other types in the library inherit from DirectXMath types, so you need to be using DirectXMath 3.17b or later for C++20 spaceship operator support for Matrix, Vector2, etc. See this blog post.

Remarks

SimpleMath is included in the DirectX Tool Kit for DX11 / DX12, but the implementation is fully standalone requiring only DirectXMath. You could use these source files directly in a project if you do not want to use the rest of the toolkit: SimpleMath.h, SimpleMath.inl, and SimpleMath.cpp.

In addition to the compiler toolsets supported by the DirectX Tool Kit, SimpleMath is also supported on Windows Subsystem for Linux (WSL) using GCC 9.4, 11.3.

If you are looking for an equivalent to the D3DXmath D3DXCreateMatrixStack / ID3DXMATRIXStack, see DirectXMatrixStack.h.

Further Reading

DirectXMath project wiki

SimpleMath - a simplified wrapper for DirectXMath