XMFLOATなのかXMVECTORなのか問題
XMFLOATなのかXMVECTORなのか問題 XMFLOAT3とXMVECTORは何が違うのか。 調べてまとめてみた。
DirectXMathの使い分けを調べてみた
DirectXMathを使い始めると、最初に迷いやすいのがXMVECTORとXMFLOAT3の使い分けです。
どちらも3次元ベクトルを扱えそうに見えますが、DirectXMathの関数の引数などを見るとどうやらなんかしらの使い分けがありそうです。
公式のドキュメントなどを参照してみると、基本方針は次のとおりの様です。
XMFLOAT3はデータを保存するための型、XMVECTORは計算するための型として使う。
クラスや構造体のメンバ変数にはXMFLOAT3を使い、計算が必要になったらXMLoadFloat3()でXMVECTORへ読み込みます。計算が終わったら、XMStoreFloat3()で結果をXMFLOAT3へ戻します。
XMFLOAT3に保存
↓ XMLoadFloat3()
XMVECTORで計算
↓ XMStoreFloat3()
XMFLOAT3に保存
そこで、この記事ではXMVECTORとXMFLOAT3の基本的な違いから、XMLoadFloat3()とXMStoreFloat3()、ゲーム内での移動計算、Transformクラスでの設計例まで順番に説明します。
記事の最後には、公式ドキュメントでよく見かけるFXMVECTOR、GXMVECTOR、HXMVECTOR、CXMVECTOR、XM_CALLCONVについての付録も用意しています。これらはおまけとして調べてみましたので、特に気にせずXMVECTORのシノニムだと思って使って良いと思います(はじめのうちは)。
この記事でわかること
XMVECTORとXMFLOAT3の違い- 保存用と計算用を分ける理由
XMLoadFloat3()とXMStoreFloat3()の使い方- ゲームオブジェクトの位置を更新する実装例
- 位置・方向・法線を行列変換するときの違い
Transformクラスのメンバ型と関数設計- 付録:
FXMVECTOR系とXM_CALLCONVが必要な理由 - 頂点バッファや定数バッファでの使い方
XMVECTORとXMFLOAT3の違い
まずは、それぞれの役割を比較します。
| 項目 | XMVECTOR |
XMFLOAT3 |
|---|---|---|
| 主な用途 | ベクトル計算 | データの保存 |
| 要素数 | 4要素 | 3要素 |
| SIMD演算 | DirectXMath関数で利用する | そのままでは利用しない |
| メンバへのアクセス | 基本的に専用関数を使う | .x、.y、.zでアクセスできる |
| クラスのメンバ | 一時的な計算値向き | 状態の保持に向く |
| 頂点データ | 通常は使わない | 位置や法線に使いやすい |
| ファイル保存 | 扱いにくい | 扱いやすい |
| 関数内の中間計算 | 向いている | DirectXMath関数を使うにはロードが必要 |
簡単に表現するなら、次のように考えるとわかりやすいでしょう。
XMFLOAT3は倉庫、XMVECTORは作業台。
ゲームオブジェクトの位置はXMFLOAT3として倉庫に保存しておきます。移動や回転などの計算が必要になったときだけ、XMVECTORという作業台へ持ってきて処理します。
つまり、ベクトルの3または4要素をまるっと一度に計算する場面では、XMVECTOR使ったほうが、効率いいし速そうだよ。ってことです。
XMVECTORとは
XMVECTORは、DirectXMathライブラリでベクトル演算を行うための中心的な型です。
#include <DirectXMath.h>
using namespace DirectX;
XMVECTOR a = XMVectorSet(1.0f, 2.0f, 3.0f, 0.0f);
XMVECTOR b = XMVectorSet(4.0f, 5.0f, 6.0f, 0.0f);
XMVECTOR result = XMVectorAdd(a, b);
XMVECTORは、4個の32ビット値をまとめて扱います。
x, y, z, w
SSEを使用する環境では__m128、ARM NEONを使用する環境ではfloat32x4_tなど、対象環境に応じたSIMD型として定義されます。
DirectXMathの現在のヘッダーでは、概念的に次のように切り替えられています。
// SSEを使用する環境
using XMVECTOR = __m128;
// ARM NEONを使用する環境
using XMVECTOR = float32x4_t;
そのため、XMVECTORを普通の構造体だと考えて、常に次のようなアクセスができるとは限りません。
// 基本的には、このような直接アクセスを前提にしない
vector.x = 10.0f;
要素を取得するときは、DirectXMathのアクセサ関数を使用します。
float x = XMVectorGetX(vector);
float y = XMVectorGetY(vector);
float z = XMVectorGetZ(vector);
float w = XMVectorGetW(vector);
値を変更するときも、専用関数を使用できます。
vector = XMVectorSetX(vector, 10.0f);
vector = XMVectorSetY(vector, 20.0f);
ただし、個々の成分を何度も取り出して計算するのであれば、そもそもXMVECTORを使う意味が薄くなります。XMVECTORは、DirectXMathのベクトル関数を使って、複数の成分をまとめて処理するための型です。
XMFLOAT2・XMFLOAT3・XMFLOAT4とは
XMFLOAT系は、浮動小数点数をメモリ上に保存するための構造体です。
代表的な型には、次のものがあります。
XMFLOAT2 uv;
XMFLOAT3 position;
XMFLOAT4 color;
XMFLOAT4 quaternion;
XMFLOAT4X4 matrix;
XMFLOAT3は、実質的に次のような構造です。
struct XMFLOAT3
{
float x;
float y;
float z;
};
そのため、通常のC++の構造体と同じ感覚で扱えます。
XMFLOAT3 position(10.0f, 20.0f, 30.0f);
position.x += 5.0f;
position.y = 0.0f;
XMFLOAT3が向いている主な用途は次のとおりです。
- ゲームオブジェクトの位置・回転・スケールの保持
- 頂点バッファの位置・法線データ
- ファイルへの保存と読み込み
- エディターやデバッグ画面への表示
- ネットワーク送信用のデータ
XMVECTORで計算した結果の格納
XMFLOAT3は保存しやすく、メンバへ直接アクセスできます。ただし、XMVectorAdd()やXMVector3Normalize()などの関数へ、そのまま渡すことはできません。
XMFLOAT3 position(1.0f, 2.0f, 3.0f);
// XMFLOAT3をそのまま渡すことはできない
// XMVECTOR result = XMVector3Normalize(position);
DirectXMathのベクトル関数で計算するときは、XMVECTORへロードします。
XMLoadFloat3とXMStoreFloat3
XMFLOAT3とXMVECTORの間では、ロード関数とストア関数を使用します。
XMFLOAT3からXMVECTORへ読み込む
XMFLOAT3 position(1.0f, 2.0f, 3.0f);
XMVECTOR positionVector = XMLoadFloat3(&position);
XMLoadFloat3()は、XMFLOAT3のx、y、zをXMVECTORへ読み込みます。
XMFLOAT3にはwが存在しないため、ロード後のw成分は0.0fになります。
XMFLOAT3 XMVECTOR
x = 1.0 → x = 1.0
x = 2.0 → y = 2.0
z = 3.0 → z = 3.0
w = 0.0
XMVECTORからXMFLOAT3へ保存する
XMVECTOR positionVector = XMVectorSet(1.0f, 2.0f, 3.0f, 0.0f);
XMFLOAT3 position;
XMStoreFloat3(&position, positionVector);
XMStoreFloat3()は、XMVECTORのx、y、zをXMFLOAT3へ書き込みます。w成分は保存されません。
基本的なデータの流れ
XMFLOAT3 position(1.0f, 2.0f, 3.0f);
XMFLOAT3 velocity(4.0f, 0.0f, 0.0f);
// 1. 保存用データを計算用の型へロード
XMVECTOR p = XMLoadFloat3(&position);
XMVECTOR v = XMLoadFloat3(&velocity);
// 2. XMVECTORで計算
p = XMVectorAdd(p, v);
// 3. 計算結果を保存用データへストア
XMStoreFloat3(&position, p);
これが、DirectXMathを使うときの基本パターンです。
プレイヤーの位置を更新する実装例
ゲーム内で実際に使う形にすると、次のようになります。
#include <DirectXMath.h>
class Player
{
private:
DirectX::XMFLOAT3 position_;
DirectX::XMFLOAT3 velocity_;
public:
Player()
: position_(0.0f, 0.0f, 0.0f),
velocity_(3.0f, 0.0f, 0.0f)
{
}
void Update(float deltaTime)
{
using namespace DirectX;
XMVECTOR position = XMLoadFloat3(&position_);
XMVECTOR velocity = XMLoadFloat3(&velocity_);
// position = velocity * deltaTime + position
position = XMVectorMultiplyAdd(
velocity,
XMVectorReplicate(deltaTime),
position
);
XMStoreFloat3(&position_, position);
}
const DirectX::XMFLOAT3& GetPosition() const
{
return position_;
}
};
このクラスでは、位置と速度をXMFLOAT3で保持しています。
DirectX::XMFLOAT3 position_;
DirectX::XMFLOAT3 velocity_;
Update()内では、計算が必要な間だけXMVECTORへロードしています。
XMVECTOR position = XMLoadFloat3(&position_);
XMVECTOR velocity = XMLoadFloat3(&velocity_);
計算が終わったら、位置だけをメンバ変数へ戻します。
XMStoreFloat3(&position_, position);
このように、長期間保持するデータと、一時的な計算データを分けるのが基本です。
ロードとストアは計算の前後にまとめる
次のコードは動作しますが、ロードとストアを何度も繰り返しています。
XMVECTOR position = XMLoadFloat3(&position_);
position = XMVectorAdd(position, XMLoadFloat3(&velocity_));
XMStoreFloat3(&position_, position);
position = XMLoadFloat3(&position_);
position = XMVectorAdd(position, XMLoadFloat3(&acceleration_));
XMStoreFloat3(&position_, position);
できるだけ、計算の最初に必要な値をロードし、計算が完了してからまとめてストアします。
XMVECTOR position = XMLoadFloat3(&position_);
XMVECTOR velocity = XMLoadFloat3(&velocity_);
XMVECTOR acceleration = XMLoadFloat3(&acceleration_);
position = XMVectorAdd(position, velocity);
position = XMVectorAdd(position, acceleration);
XMStoreFloat3(&position_, position);
XMVECTORは、レジスタ上で連続した計算を行いやすいように設計されています。途中で何度もメモリへ戻すより、必要な計算をまとめてから結果を保存する方が、コードの意図も明確になります。
ただし、ロードとストアを1回でも行ったら遅い、という意味ではありません。最適化では、実際の処理時間をReleaseビルドで計測することが重要です。
XMVECTORのw成分は何に使うのか
XMVECTORは、3次元ベクトルを扱う場合でもx、y、z、wの4成分を持ちます。
XMVECTOR value = XMVectorSet(x, y, z, w);
一般的な同次座標では、位置と方向を次のように表します。
| 用途 | x | y | z | w |
|---|---|---|---|---|
| 位置 | px | py | pz | 1 |
| 方向 | dx | dy | dz | 0 |
| 色 | r | g | b | a |
| クォータニオン | x | y | z | w |
ただし、DirectXMathの関数によっては、入力されたwをそのまま使わず、関数側で位置または方向として扱います。
位置を変換するXMVector3TransformCoord
XMVECTOR worldPosition =
XMVector3TransformCoord(localPosition, worldMatrix);
XMVector3TransformCoord()は入力のw成分を無視し、w = 1.0fとして変換します。そのため、行列の平行移動成分の影響を受けます。
返されるベクトルは、結果がw = 1.0fになるように射影除算されます。
方向や法線を変換するXMVector3TransformNormal
XMVECTOR worldDirection =
XMVector3TransformNormal(localDirection, worldMatrix);
XMVector3TransformNormal()は、方向ベクトルや法線ベクトルを変換するときに使います。平行移動成分は計算に含まれません。
したがって、用途によって使い分けます。
頂点の位置 → XMVector3TransformCoord
移動方向 → XMVector3TransformNormal
法線ベクトル → XMVector3TransformNormal
なお、非一様スケールを含む行列で法線を正しく変換する場合は、ワールド行列そのものではなく、逆転置行列を使用する必要があります。
Transformクラスではどう設計するか
ゲームエンジンのTransformクラスでは、位置・回転・スケールを長期間保持します。
そのため、メンバ変数にはXMFLOAT3を使用する設計が扱いやすいでしょう。
#pragma once
#include <DirectXMath.h>
class Transform
{
private:
DirectX::XMFLOAT3 position_;
DirectX::XMFLOAT3 rotation_;
DirectX::XMFLOAT3 scale_;
public:
Transform()
: position_(0.0f, 0.0f, 0.0f),
rotation_(0.0f, 0.0f, 0.0f),
scale_(1.0f, 1.0f, 1.0f)
{
}
const DirectX::XMFLOAT3& GetPosition() const
{
return position_;
}
DirectX::XMVECTOR GetPositionVector() const
{
return DirectX::XMLoadFloat3(&position_);
}
void SetPosition(float x, float y, float z)
{
position_ = DirectX::XMFLOAT3(x, y, z);
}
DirectX::XMMATRIX GetWorldMatrix() const
{
using namespace DirectX;
const XMMATRIX scale = XMMatrixScalingFromVector(
XMLoadFloat3(&scale_)
);
const XMMATRIX rotation = XMMatrixRotationRollPitchYawFromVector(
XMLoadFloat3(&rotation_)
);
const XMMATRIX translation = XMMatrixTranslationFromVector(
XMLoadFloat3(&position_)
);
return scale * rotation * translation;
}
};
この設計では、用途に応じて2種類の取得方法を用意しています。
// 保存データとして参照したい
const XMFLOAT3& GetPosition() const;
// DirectXMathで計算したい
XMVECTOR GetPositionVector() const;
この形なら、ゲームロジック側では保存用のXMFLOAT3として位置を確認でき、数学処理側ではGetPositionVector()から計算用のXMVECTORを取得できます。
XMVECTORを直接受け取る自作関数の書き方は、記事末尾の付録で説明します。
頂点バッファではXMFLOATを使う
頂点データは、連続したメモリ上に大量に並べてGPUへ送ります。
そのため、通常はXMVECTORではなくXMFLOAT系を使用します。
struct Vertex
{
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT2 uv;
};
頂点データの位置をCPU側で変換するときは、ロード・計算・ストアを行います。
void TransformVertex(
Vertex& vertex,
const DirectX::XMMATRIX& matrix
) noexcept
{
using namespace DirectX;
XMVECTOR position = XMLoadFloat3(&vertex.position);
position = XMVector3TransformCoord(position, matrix);
XMStoreFloat3(&vertex.position, position);
XMVECTOR normal = XMLoadFloat3(&vertex.normal);
normal = XMVector3TransformNormal(normal, matrix);
normal = XMVector3Normalize(normal);
XMStoreFloat3(&vertex.normal, normal);
}
実際の描画では、毎フレーム全頂点をCPUで変換するより、頂点シェーダーで変換する設計が一般的です。この例は、XMFLOAT3とXMVECTORの役割を示すためのものです。
定数バッファではC++側とHLSL側の配置を合わせる
定数バッファへ値を送るときも、保存用の構造体にはXMFLOAT4やXMFLOAT4X4を使用できます。
struct SceneConstantBuffer
{
DirectX::XMFLOAT4X4 world;
DirectX::XMFLOAT4 color;
DirectX::XMFLOAT3 lightDirection;
float padding;
};
行列はXMMATRIXで計算し、XMFLOAT4X4へ保存します。
DirectX::XMMATRIX world = transform.GetWorldMatrix();
SceneConstantBuffer buffer{};
DirectX::XMStoreFloat4x4(
&buffer.world,
DirectX::XMMatrixTranspose(world)
);
HLSLの定数バッファでは、変数が16バイト境界をまたがないように配置されます。そのため、C++側の構造体とHLSL側のcbufferで、サイズと並びを対応させる必要があります。
cbuffer SceneConstantBuffer : register(b0)
{
float4x4 world;
float4 color;
float3 lightDirection;
float padding;
};
XMFLOAT3の直後にfloatを置く形はよく使われますが、常に機械的にpaddingを追加すればよいわけではありません。HLSL側の変数順序とパッキング規則を確認し、C++側と一致させます。
XMFLOAT3Aとは
DirectXMathには、XMFLOAT3を16バイト境界へアラインしたXMFLOAT3Aもあります。
DirectX::XMFLOAT3A alignedPosition;
XMFLOAT3Aのロードとストアには、対応する関数を使用します。
DirectX::XMVECTOR position =
DirectX::XMLoadFloat3A(&alignedPosition);
DirectX::XMStoreFloat3A(
&alignedPosition,
position
);
対応関係は次のとおりです。
| 保存型 | ロード | ストア |
|---|---|---|
XMFLOAT3 |
XMLoadFloat3() |
XMStoreFloat3() |
XMFLOAT3A |
XMLoadFloat3A() |
XMStoreFloat3A() |
XMFLOAT4 |
XMLoadFloat4() |
XMStoreFloat4() |
XMFLOAT4A |
XMLoadFloat4A() |
XMStoreFloat4A() |
アライン済みの型を使えば常に速くなるとは限りません。データサイズ、配列の配置、キャッシュ効率、コンパイラの最適化なども関係します。
また、アライン済み型をSTLコンテナで使用する場合は、必要なアラインメントが維持されるかを確認する必要があります。
通常のゲームオブジェクトの位置を保持するだけなら、まずはXMFLOAT3で十分です。実際にボトルネックが確認できた部分で、アライン済み型やデータ配置を検討する方が安全です。
よくある間違い
XMVECTORを常にクラスのメンバにする
XMVECTORをメンバ変数にすること自体が禁止されているわけではありません。
ただし、次の用途ではXMFLOAT3の方が扱いやすいことが多いです。
- ファイルへ保存する
- GUIへ表示する
- 頂点配列へ格納する
- 構造体をコピーする
- 個別成分を編集する
- C++とHLSLのデータ配置を合わせる
特別な理由がなければ、ゲームオブジェクトの状態はXMFLOAT系で保持し、計算中だけXMVECTORを使う形から始めるとよいでしょう。
XMFLOAT3を成分ごとに計算し続ける
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
position.z += velocity.z * deltaTime;
このコードが間違いというわけではありません。単純な処理なら十分に読みやすく、コンパイラが最適化する可能性もあります。
ただし、正規化、内積、外積、行列変換、クォータニオン回転などを行う場合は、DirectXMathの関数を使用した方が処理を統一できます。
位置と法線へ同じ変換関数を使う
位置にはXMVector3TransformCoord()、方向や法線にはXMVector3TransformNormal()を使用します。
ただし、法線については非一様スケールを含む場合に逆転置行列が必要です。
判断に迷ったときの使い分け
XMFLOAT系を使う場面
- クラスや構造体のメンバとして保存する
- 頂点データへ格納する
- ファイルへ保存する
- エディターで編集する
- C++とHLSLの構造体を対応させる
- ベクトルの成分へ直接アクセスする
XMVECTORを使う場面
- ベクトルの加算・減算・乗算を行う
- 正規化する
- 内積・外積を求める
- 行列で座標変換する
- クォータニオンで回転する
- 関数内で一時的な計算結果を保持する
最終的なデータフロー
DirectXMathを使うときは、次の流れを基本にすると整理しやすくなります。
1. XMFLOAT3で状態を保持する
↓
2. XMLoadFloat3()でXMVECTORへロードする
↓
3. DirectXMathの関数で計算する
↓
4. XMStoreFloat3()でXMFLOAT3へ戻す
この「保存 → ロード → 計算 → ストア」が、この記事で最も重要な基本形です。
自作関数でXMVECTORを受け取るときの書き方は、記事末尾の付録で説明します。
まとめ
XMVECTORとXMFLOAT3は、どちらか一方だけを選ぶものではありません。
それぞれの役割を分けて、一緒に使用します。
保存はXMFLOAT、計算はXMVECTOR。
基本となる役割は、次の2つです。
保存用:XMFLOAT2 / XMFLOAT3 / XMFLOAT4
計算用:XMVECTOR
- メンバ変数や頂点データでは
XMFLOAT系を使う - 計算するときだけ
XMVECTORへロードする - 計算結果は
XMFLOAT系へストアして保存する
まずは、この流れを理解すればDirectXMathを使い始められます。
公式ドキュメントに出てくるFXMVECTOR系は、通常のベクトル計算とは別の「関数の受け渡し方」の話です。必要になった段階で、後半の付録を参照してください。
FAQ
XMVECTORとXMFLOAT3は、どちらの方が速いですか?
単純に型だけで比較するものではありません。XMFLOAT3は保存用、XMVECTORはDirectXMathによる計算用です。計算内容、メモリアクセス、コンパイラ最適化、データ配置によって性能は変わります。
XMVECTORをクラスのメンバ変数にしてはいけませんか?
禁止ではありません。ただし、保存、ファイル入出力、デバッグ表示、頂点データなどではXMFLOAT3の方が扱いやすいため、一般的なTransformではXMFLOAT3を保持して計算時にロードする設計がわかりやすいでしょう。
公式ドキュメントのFXMVECTORには何を渡せばよいですか?
普通のXMVECTORをそのまま渡します。FXMVECTOR型の変数を別に作ったり、変換したりする必要はありません。FXMVECTOR系は、主に関数を定義する側で使用する引数用の型エイリアスです。詳しい理由と使い分けは、次の付録で説明します。
XMLoadFloat3でw成分はどうなりますか?
XMLoadFloat3()で読み込んだXMVECTORのw成分は0.0fになります。ただし、XMVector3TransformCoord()は入力のwを無視し、1.0fとして位置変換を行います。
付録:FXMVECTOR・GXMVECTOR・HXMVECTOR・CXMVECTORとは
ここからは、DirectXMathの公式ドキュメントやヘッダーファイルを読んだときに登場する、少し高度な引数型について説明します。
通常のベクトル計算だけを行う場合は、ここまでの内容を理解していれば十分です。
最初に結論:関数を使う側はXMVECTORを渡せばよい
公式ドキュメントでは、関数の引数がFXMVECTORになっていることがあります。
XMVECTOR XM_CALLCONV XMVector3Normalize(
FXMVECTOR V
) noexcept;
これを見ると、FXMVECTOR型の変数を作らなければならないように見えます。
しかし、関数を呼び出す側が用意するのは、普通のXMVECTORです。
using namespace DirectX;
XMVECTOR direction =
XMVectorSet(1.0f, 2.0f, 3.0f, 0.0f);
XMVECTOR normalized =
XMVector3Normalize(direction);
変換処理は必要ありません。
関数を呼び出す側:XMVECTORを渡す
関数を定義する側:FXMVECTORなどで受け取る
FXMVECTOR、GXMVECTOR、HXMVECTOR、CXMVECTORは、位置・方向・法線・色などを表す型ではありません。
すべて、関数がXMVECTORを受け取るときに使用する型エイリアスです。
普通のC++ではコンパイラに任せるのではないか
ここで、次の疑問が出てきます。
普通の構造体を関数へ渡すとき、レジスタを使うかメモリを使うかはコンパイラが決める。XMVECTORだけ、なぜ特別な型を使う必要があるのか。
この疑問はもっともです。
実際に、どのレジスタを使うか、あるいはメモリを使うかという最終的な判断は、DirectXMathでもコンパイラが行います。
FXMVECTORは、「この値を必ず特定のレジスタへ入れろ」と命令する型ではありません。
DirectXMathが切り替えたいのは、その一つ前の段階にある、次の違いです。
// XMVECTORそのものを値として受け取る
void Function(XMVECTOR value);
// 元のXMVECTORを参照して受け取る
void Function(const XMVECTOR& value);
値渡しと参照渡しは、C++の関数宣言として別物です。
たとえるなら、次の違いです。
値渡し :荷物そのものを渡す
参照渡し:荷物が置かれている場所を渡す
コンパイラは、「荷物をどの車に載せるか」に相当するレジスタやメモリの使い方は判断できます。
しかし、プログラマーが「荷物そのものを渡す」と宣言したのに、コンパイラが勝手に「荷物の置き場所だけを渡す」方式へ変更することはできません。呼び出す側と受け取る側が、同じ方法を前提にしていなければならないためです。
つまり、コンパイラが自由に決められるのは、宣言された値渡し・参照渡しと、その環境の呼び出し規約を守った範囲内です。
なぜXMFLOAT4では同じことを意識しないのか
XMFLOAT4は、4個のfloatをメモリへ保存するための通常の構造体です。
struct XMFLOAT4
{
float x;
float y;
float z;
float w;
};
次のような関数を書いた場合、引数を実際にどのレジスタやメモリで渡すかは、通常のC++の呼び出し規約に従ってコンパイラが決めます。
void Function(
XMFLOAT4 a,
XMFLOAT4 b,
XMFLOAT4 c
);
一方、XMVECTORは単なる保存用構造体ではありません。
x86・x64環境では__m128、ARM環境ではNEONのベクトル型などに対応する、SIMD計算用の型です。
XMFLOAT4:メモリへ保存するための普通の構造体
XMVECTOR:SIMD演算で使用するための特殊なベクトル型
XMFLOAT4を関数へ渡した場合、関数内でSIMD計算をするには、最終的にXMVECTORへロードする必要があります。
XMFLOAT4 value;
XMVECTOR vector = XMLoadFloat4(&value);
DirectXMathの関数は、最初からXMVECTORを受け取り、そのままSIMD演算へ使用するように作られています。
そのためDirectXMathでは、一般的な構造体よりも、XMVECTORを関数の境界でどう受け取るかが性能へ影響しやすくなります。
DirectXMathが解決したい本当の問題
XMVECTORに適した受け取り方は、すべての環境で同じではありません。
環境によっては、XMVECTORを値として受け取ることで、ベクトルレジスタを使った受け渡しができます。
const XMVECTOR value
別の環境や引数位置では、参照として受け取った方が、その環境の呼び出し規約に適しています。
const XMVECTOR& value
この違いには、次のような条件が関係します。
- x86、x64、ARM、ARM64の違い
- 32ビットと64ビットの違い
- 使用するコンパイラ
__fastcallや__vectorcallなどの呼び出し規約- その関数が受け取る
XMVECTORの数と位置
DirectXMathは、Windowsの複数のCPU環境で同じヘッダーファイルを利用できる、移植可能なSIMDライブラリとして作られています。
もし型エイリアスがなければ、関数定義を環境ごとに書き分ける必要があります。
#if VALUE_PASS_IS_BETTER
XMVECTOR Function(const XMVECTOR value);
#else
XMVECTOR Function(const XMVECTOR& value);
#endif
これを一つの書き方へまとめるために、DirectXMathはFXMVECTORなどの名前を用意しています。
XMVECTOR XM_CALLCONV Function(
FXMVECTOR value
) noexcept;
FXMVECTORの実体は、環境に応じて概念的に次のどちらかへ切り替わります。
typedef const XMVECTOR FXMVECTOR;
または、
typedef const XMVECTOR& FXMVECTOR;
つまりFXMVECTORは、レジスタを直接指定するための型ではありません。
XMVECTORを値として受け取るか、参照として受け取るかというプラットフォーム差を隠すための型と考えると理解しやすいでしょう。
その宣言を基に、実際にどのレジスタやメモリを使うかをコンパイラが決定します。
なぜFX・GX・HX・CXの4種類があるのか
値渡しが適している範囲は、環境や呼び出し規約だけでなく、その関数に何個目のXMVECTORとして渡されるかによっても変わります。
その違いを一つのFXMVECTORだけでは表せないため、DirectXMathでは4種類のエイリアスを用意しています。
XMVECTOR入力引数としての順番 |
使用する型 |
|---|---|
| 1~3番目 | FXMVECTOR |
| 4番目 | GXMVECTOR |
| 5~6番目 | HXMVECTOR |
| 7番目以降 | CXMVECTOR |
覚え方は次のとおりです。
1~3番目 :F
4番目 :G
5~6番目 :H
7番目以降:C
これらはベクトルの意味を表しているわけではありません。
FXMVECTOR = 位置用
GXMVECTOR = 方向用
HXMVECTOR = 法線用
CXMVECTOR = 色用
という分類ではありません。
呼び出し側が渡す変数は、すべて普通のXMVECTORです。
XMVECTOR v1;
XMVECTOR v2;
XMVECTOR v3;
XMVECTOR v4;
XMVECTOR v5;
XMVECTOR v6;
XMVECTOR v7;
XMVECTOR result = Calculate(
v1, v2, v3, v4, v5, v6, v7
);
関数を定義する側だけが、次のようにエイリアスを使い分けます。
XMVECTOR XM_CALLCONV Calculate(
FXMVECTOR v1,
FXMVECTOR v2,
FXMVECTOR v3,
GXMVECTOR v4,
HXMVECTOR v5,
HXMVECTOR v6,
CXMVECTOR v7
) noexcept;
数えるのは関数全体の何番目ではない
数えるのは、関数全体の引数番号ではありません。
入力として受け取るXMVECTORが何個目かを数えます。
XMVECTOR XM_CALLCONV MovePosition(
float deltaTime,
FXMVECTOR position,
float speed,
FXMVECTOR direction
) noexcept;
この例では、positionは関数全体では2番目の引数です。しかし、XMVECTOR入力としては1番目なのでFXMVECTORです。
directionは関数全体では4番目ですが、XMVECTOR入力としては2番目なので、同じくFXMVECTORです。
1個目のXMVECTOR入力:position → FXMVECTOR
2個目のXMVECTOR入力:direction → FXMVECTOR
float、int、boolなどは数えません。
4個目のXMVECTOR入力だけがGXMVECTORになります。
XMVECTOR XM_CALLCONV SampleFunction(
float time,
FXMVECTOR a,
int mode,
FXMVECTOR b,
FXMVECTOR c,
bool enabled,
GXMVECTOR d
) noexcept;
dは関数全体では7番目ですが、XMVECTOR入力としては4番目なのでGXMVECTORです。
出力引数は数えない
計算結果を書き込むためのXMVECTOR*やXMVECTOR&は、入力引数の数に含めません。
たとえば、XMVectorSinCos()は次のような宣言です。
void XM_CALLCONV XMVectorSinCos(
XMVECTOR* pSin,
XMVECTOR* pCos,
FXMVECTOR V
) noexcept;
pSinとpCosは、計算結果を書き込む出力先です。
入力として渡されるXMVECTORはVだけなので、Vは1個目としてFXMVECTORになります。
XMVECTOR* pSin:出力なので数えない
XMVECTOR* pCos:出力なので数えない
FXMVECTOR V :1個目のXMVECTOR入力
XM_CALLCONVとは
FXMVECTOR系のエイリアスは、値渡しと参照渡しの違いを環境に合わせて切り替えます。
一方、XM_CALLCONVは、関数全体の呼び出し規約を環境に合わせて切り替えるマクロです。
XMVECTOR XM_CALLCONV MovePosition(
FXMVECTOR position,
FXMVECTOR velocity,
float deltaTime
) noexcept;
現在のDirectXMathでは、環境に応じて__vectorcall、__fastcall、または何も付けない形などへ切り替わります。
FXMVECTOR系:各XMVECTOR引数をどの型で受け取るか
XM_CALLCONV:関数全体をどの呼び出し規約で呼ぶか
DirectXMathと同じ形式で自作関数を定義する場合は、基本的にセットで使用します。
ヘッダーファイル
DirectX::XMVECTOR XM_CALLCONV MovePosition(
DirectX::FXMVECTOR position,
DirectX::FXMVECTOR velocity,
float deltaTime
) noexcept;
CPPファイル
DirectX::XMVECTOR XM_CALLCONV MovePosition(
DirectX::FXMVECTOR position,
DirectX::FXMVECTOR velocity,
float deltaTime
) noexcept
{
return DirectX::XMVectorMultiplyAdd(
velocity,
DirectX::XMVectorReplicate(deltaTime),
position
);
}
宣言と定義の両方で、同じXM_CALLCONVを使用します。
なお、既存のDirectXMath関数を呼び出すだけなら、利用者がXM_CALLCONVを書く必要はありません。
コンストラクタだけ規則が異なる
C++のコンストラクタでは、通常の関数と同じ規則をそのまま使いません。
DirectXMathの公式ガイドでは、コンストラクタにGXMVECTORとHXMVECTORを使用しないことが推奨されています。
最初の3個をFXMVECTOR、4個目以降をCXMVECTORにします。
class VectorCollection
{
public:
VectorCollection(
DirectX::FXMVECTOR v1,
DirectX::FXMVECTOR v2,
DirectX::FXMVECTOR v3,
DirectX::CXMVECTOR v4,
DirectX::CXMVECTOR v5
);
};
通常の関数は次の並びです。
F, F, F, G, H, H, C, C, ...
コンストラクタでは次の並びになります。
F, F, F, C, C, C, C, ...
また、コンストラクタにはXM_CALLCONVを付けないことが推奨されています。
戻り値はXMVECTORでよい
FXMVECTOR系は、入力引数を受け取るための型です。
戻り値には通常のXMVECTORを使用します。
DirectX::XMVECTOR XM_CALLCONV GetForwardVector(
DirectX::FXMVECTOR rotationQuaternion
) noexcept
{
const DirectX::XMVECTOR baseForward =
DirectX::XMVectorSet(
0.0f,
0.0f,
1.0f,
0.0f
);
return DirectX::XMVector3Rotate(
baseForward,
rotationQuaternion
);
}
自作関数では必ず使わなければならないのか
既存のDirectXMath関数を呼び出すだけなら、FXMVECTOR系を自分で書く場面はありません。
また、自分のアプリケーション内部だけで使う単純な関数なら、通常のC++として次のように書くこともできます。
XMVECTOR NormalizeVector(XMVECTOR value);
XMVECTOR NormalizeVector(const XMVECTOR& value);
これらが直ちに間違いになるわけではありません。
ただし、DirectXMathと同じように、複数のWindowsプラットフォームや呼び出し規約へ対応しながら、XMVECTORの受け渡しを最適化した形式にしたい場合は、FXMVECTOR系とXM_CALLCONVを使用します。
つまり、利用者側では次のように判断できます。
DirectXMath関数を呼ぶだけ
→ 普通のXMVECTORを渡す
簡単な自作関数を作る
→ 通常のC++の引数でも書ける
DirectXMathと同じ移植性・呼び出し規約を採用する
→ FXMVECTOR系 + XM_CALLCONVを使う
XMMATRIXにも同じ考え方がある
行列にも、関数引数用の型エイリアスがあります。
FXMMATRIX
CXMMATRIX
これらも、行列の意味を表す別の型ではありません。XMMATRIXを関数が受け取るときの環境差を隠すための型です。
ただし、FXMMATRIXの細かい規則は、周囲にあるベクトル引数や浮動小数点引数の数にも影響されます。
行列の引数規則まで必要になった場合は、公式の関数宣言を参考にするか、別の記事として扱った方が整理しやすいでしょう。
付録のまとめ
FXMVECTOR系について、利用者側で最初に覚えることは次の3点です。
- 公式ドキュメントに
FXMVECTORと書かれていても、呼び出す側は普通のXMVECTORを渡す FXMVECTOR系は、値渡しと参照渡しのプラットフォーム差を隠すための型エイリアスである- DirectXMathと同じ形式の自作関数では、
FXMVECTOR系とXM_CALLCONVを組み合わせる
XMVECTOR
実際のベクトルデータ
FXMVECTOR / GXMVECTOR / HXMVECTOR / CXMVECTOR
関数がXMVECTORを受け取るための型エイリアス
XM_CALLCONV
関数全体の呼び出し規約を選ぶマクロ
細かい順番は、次のとおりです。
1~3個目のXMVECTOR入力:FXMVECTOR
4個目 :GXMVECTOR
5~6個目 :HXMVECTOR
7個目以降 :CXMVECTOR
ただし、コンストラクタでは最初の3個をFXMVECTOR、4個目以降をCXMVECTORとし、XM_CALLCONVは付けません。