【C++】DirectX9スキニングの実装手順と高速化テクニック
DirectX9のスキニングでは、頂点が複数のボーン行列とウェイトを用いてシェーダで変換され、CPU負荷を抑えつつ自然な関節変形を実現できます。
ボーン行列を配列で渡し、各頂点で線形補間することで滑らかなアニメーションが得られます。
スキニングの基礎知識
スキンメッシュとボーンの関係
スキニングは3Dキャラクターのアニメーションで重要な技術で、キャラクターのメッシュ(皮膚)をボーン(骨格)に基づいて変形させる手法です。
スキンメッシュとは、ボーンに連動して動くメッシュのことを指します。
ボーンはキャラクターの関節や骨格を模した仮想的な構造で、これを動かすことでメッシュの変形を制御します。
具体的には、ボーンは階層構造を持ち、親ボーンの動きは子ボーンに伝播します。
例えば、腕のボーンを動かすと、その子ボーンである手のボーンも連動して動きます。
スキンメッシュの各頂点は、このボーンの動きに応じて位置や法線が変化し、自然な関節の動きを表現します。
この関係を図で表すと以下のようになります。
ボーン階層例 | 説明 |
---|---|
ルートボーン | キャラクターの中心骨格 |
胴体ボーン | ルートの子、胴体の動き |
腕ボーン | 胴体の子、腕の動きを制御 |
手ボーン | 腕の子、手の動きを制御 |
ボーンの変換行列を用いて、スキンメッシュの頂点はボーンの動きに合わせて変形します。
これにより、キャラクターの動きが滑らかでリアルになります。
ウェイト割り当ての仕組み
スキニングでは、各頂点が複数のボーンの影響を受けることが一般的です。
頂点ごとに「ウェイト」と呼ばれる重みが割り当てられ、どのボーンがどの程度その頂点に影響を与えるかを示します。
ウェイトの合計は通常1.0(100%)になるように正規化されます。
例えば、腕の付け根に近い頂点は、腕ボーンのウェイトが0.7、胴体ボーンのウェイトが0.3といった具合に割り当てられます。
これにより、腕を動かすとその頂点は70%腕ボーンの動きに従い、30%胴体ボーンの動きに従う形で変形します。
ウェイト割り当ては、モデリングツールやアニメーションツールで自動的に計算されることが多いですが、手動で調整することもあります。
適切なウェイト割り当ては、関節の滑らかな変形や不自然なメッシュの引き伸ばしを防ぐために重要です。
ウェイトの例を表にまとめると以下のようになります。
頂点ID | ボーンAウェイト | ボーンBウェイト | ボーンCウェイト | 合計 |
---|---|---|---|---|
1 | 0.6 | 0.4 | 0.0 | 1.0 |
2 | 0.3 | 0.5 | 0.2 | 1.0 |
3 | 1.0 | 0.0 | 0.0 | 1.0 |
線形ブレンドとデュアルクォータニオンの比較
スキニングの頂点変形には主に「線形ブレンドスキニング(Linear Blend Skinning, LBS)」と「デュアルクォータニオンスキニング(Dual Quaternion Skinning, DQS)」の2つの手法があります。
線形ブレンドスキニング(LBS)
LBSは最も一般的なスキニング手法で、各ボーンの変換行列をウェイトで加重平均し、その結果で頂点を変換します。
計算が比較的簡単で高速に処理できるため、DirectX 9の固定機能パイプラインや頂点シェーダーでよく使われます。
しかし、LBSには「ボーン間のねじれ(スキニングアーティファクト)」が発生しやすいという欠点があります。
特に関節部分でメッシュが不自然に潰れたり、ねじれたりすることがあります。
デュアルクォータニオンスキニング(DQS)
DQSはクォータニオンを用いて回転を補間する手法で、LBSの問題点であるねじれを大幅に軽減できます。
クォータニオンの補間は回転の滑らかさを保つため、関節の変形がより自然になります。
ただし、DQSは計算が複雑で、DirectX 9の標準的な頂点シェーダーでは実装が難しい場合があります。
パフォーマンス面でもLBSより負荷が高くなることが多いです。
特徴 | 線形ブレンドスキニング (LBS) | デュアルクォータニオンスキニング (DQS) |
---|---|---|
計算コスト | 低い | 高い |
実装の容易さ | 簡単 | 複雑 |
ねじれの発生 | 発生しやすい | ほとんど発生しない |
自然な回転補間 | 不完全 | 滑らか |
DirectX9対応 | 標準的に対応 | カスタムシェーダーが必要 |
1頂点あたりボーン数の制限と影響
DirectX 9の頂点シェーダーでは、1頂点あたりに影響を与えるボーンの数に制限があります。
一般的には最大4ボーンまでが推奨されており、これを超えるとパフォーマンス低下や実装の複雑化が起こります。
制限の理由
- 頂点シェーダーの定数レジスタ数が限られているため、多数のボーン行列を渡すことが難しい
- 計算量が増えることで描画パフォーマンスが低下します
- 頂点データのサイズが大きくなり、メモリ帯域幅の負荷が増します
影響と対策
1頂点あたりのボーン数が多いと、より細かい変形が可能ですが、パフォーマンスに悪影響を与えます。
逆にボーン数を減らすとパフォーマンスは向上しますが、変形の精度が落ちる可能性があります。
多くの場合、4ボーン以内に制限し、ウェイトの小さいボーンは切り捨てるか統合する方法が取られます。
ウェイトの合計は必ず1.0になるように再正規化します。
以下はウェイトの切り捨てと再正規化の例です。
#include <iostream>
#include <vector>
#include <algorithm>
struct BoneWeight {
int boneIndex;
float weight;
};
void NormalizeWeights(std::vector<BoneWeight>& weights, int maxBones) {
// ウェイトの大きい順にソート
std::sort(weights.begin(), weights.end(), [](const BoneWeight& a, const BoneWeight& b) {
return a.weight > b.weight;
});
// maxBonesを超えるウェイトを切り捨て
if (weights.size() > maxBones) {
weights.resize(maxBones);
}
// ウェイトの合計を計算
float totalWeight = 0.0f;
for (const auto& w : weights) {
totalWeight += w.weight;
}
// 合計が0の場合は均等割り当て
if (totalWeight == 0.0f) {
float equalWeight = 1.0f / weights.size();
for (auto& w : weights) {
w.weight = equalWeight;
}
return;
}
// ウェイトを正規化
for (auto& w : weights) {
w.weight /= totalWeight;
}
}
int main() {
std::vector<BoneWeight> weights = {
{0, 0.5f},
{1, 0.3f},
{2, 0.15f},
{3, 0.05f},
{4, 0.02f} // 5つ目のボーンは切り捨て対象
};
NormalizeWeights(weights, 4);
for (const auto& w : weights) {
std::cout << "BoneIndex: " << w.boneIndex << ", Weight: " << w.weight << std::endl;
}
return 0;
}
BoneIndex: 0, Weight: 0.526316
BoneIndex: 1, Weight: 0.315789
BoneIndex: 2, Weight: 0.157895
BoneIndex: 3, Weight: 0.0
このコードでは、5つのボーンウェイトのうち、上位4つを残し、合計が1.0になるように正規化しています。
これにより、DirectX 9の頂点シェーダーで扱いやすい形に調整できます。
以上のように、1頂点あたりのボーン数制限はスキニングのパフォーマンスと品質のバランスを取る上で重要なポイントです。
適切に管理することで、リアルなアニメーションを効率的に実現できます。
DirectX9におけるスキニング処理フロー
CPU側での準備工程
DirectX9でスキニングを行う際、CPU側では主にボーンの変換行列計算と頂点データの準備を行います。
まず、アニメーションの各フレームに応じてボーンのローカル変換行列を計算し、親子関係に基づいてワールド変換行列を更新します。
これにより、ボーンの階層構造に沿った正しい位置と回転が得られます。
ボーン行列の計算は以下のような流れです。
- 各ボーンのローカル変換行列(位置・回転・スケール)を取得。
- 親ボーンのワールド変換行列と掛け合わせて、子ボーンのワールド変換行列を算出。
- バインドポーズ(初期姿勢)からのオフセット行列を掛けて、最終的なスキニング用行列を作成。
この最終行列は、頂点シェーダーに渡すためのボーン行列配列として格納します。
また、頂点データには各頂点が影響を受けるボーンのインデックスとウェイトを含める必要があります。
これらは頂点バッファに格納し、頂点シェーダーで参照されます。
CPU側での準備は、アニメーションの更新と頂点データの管理が中心です。
以下はボーン行列の階層更新の簡単な例です。
#include <vector>
#include <DirectXMath.h>
using namespace DirectX;
struct Bone {
int parentIndex;
XMMATRIX localTransform;
XMMATRIX worldTransform;
XMMATRIX offsetMatrix; // バインドポーズの逆行列
};
void UpdateBoneMatrices(std::vector<Bone>& bones) {
for (size_t i = 0; i < bones.size(); ++i) {
if (bones[i].parentIndex < 0) {
bones[i].worldTransform = bones[i].localTransform;
} else {
bones[i].worldTransform = bones[bones[i].parentIndex].worldTransform * bones[i].localTransform;
}
}
}
この関数は、親子関係に基づいてボーンのワールド変換行列を更新します。
更新後、worldTransform * offsetMatrix
を計算してスキニング用の行列を作成し、GPUに送ります。
GPU側での変換工程
GPU側では、頂点シェーダーがボーン行列と頂点のボーンインデックス・ウェイトを用いて頂点の変換を行います。
頂点シェーダーは、各頂点に対して影響を与える複数のボーン行列をウェイトで加重平均し、頂点位置や法線を変換します。
DirectX9の頂点シェーダーでは、ボーン行列は定数レジスタに配列として渡されます。
頂点データのBlendIndices
でボーン行列のインデックスを指定し、BlendWeights
でウェイトを指定します。
シェーダー内で以下のように計算します。
- 各ボーン行列に頂点位置を乗算し、ウェイトを掛けて加算
- 同様に法線も変換し、正規化
- 最終的にビュー・プロジェクション行列を掛けてスクリーン座標に変換
この処理により、ボーンの動きに応じたメッシュの変形がリアルタイムに実現されます。
固定機能パイプラインとの違い
DirectX9では固定機能パイプラインでもスキニングが可能ですが、頂点シェーダーを使う方法と比べて柔軟性と表現力に差があります。
固定機能パイプラインでは、SetTransform
関数でボーン行列を設定し、SetStreamSource
で頂点バッファを指定します。
スキニングはSetSoftwareVertexProcessing
を有効にしてCPUで処理するか、固定機能のスキニング機能を利用しますが、ボーン数やウェイト数に制限が厳しく、複雑なアニメーションには向きません。
一方、頂点シェーダーを使うと、ボーン数の増加や複雑なウェイト計算、法線の正確な変換などが可能です。
また、カスタムのライティングやエフェクトも組み込みやすくなります。
つまり、固定機能パイプラインは簡単なスキニングに適し、頂点シェーダーは高品質で柔軟なスキニングに適しています。
Shader Model 2.0の制約事項
DirectX9のShader Model 2.0(SM2.0)は、スキニング実装においていくつかの制約があります。
これらを理解しないと、パフォーマンス低下や実装困難に繋がるため注意が必要です。
主な制約は以下の通りです。
- 定数レジスタ数の制限
SM2.0では頂点シェーダーの定数レジスタが最大96個(float4単位)まで使用可能です。
ボーン行列は4×3または4×4の行列で表現されるため、扱えるボーン数は限られます。
例えば4×3行列の場合、1ボーンあたり3レジスタを消費するため、最大で約32ボーン分の行列を渡せますが、他の定数も使うため実際はもっと少なくなります。
- ループの制限
SM2.0では頂点シェーダー内のループは静的(コンパイル時に展開される)でなければなりません。
動的なループや条件分岐は制限されるため、ボーン数を固定してコードを書く必要があります。
- 命令数の制限
頂点シェーダーの命令数は最大64命令までに制限されています。
複雑なスキニング計算や多くのボーンを扱うと命令数がオーバーしやすくなります。
- テクスチャサンプリング不可
頂点シェーダーでテクスチャをサンプリングできないため、ボーン行列をテクスチャに格納して参照するテクニックは使えません。
これらの制約を踏まえ、SM2.0環境ではボーン数を4~8程度に抑え、ループ展開を行い、命令数を節約する工夫が必要です。
例えば、ボーン数を固定した複数の頂点シェーダーを用意し、描画時に切り替える方法がよく使われます。
以上の制約を理解し、適切に設計することで、DirectX9のSM2.0環境でも効率的なスキニングが可能になります。
頂点データ設計
ボーンインデックスのエンコード方法
スキニングで頂点ごとに影響を与えるボーンを指定するため、ボーンインデックスを頂点データに格納します。
DirectX9の頂点シェーダーでは、ボーンインデックスは整数値として扱われ、頂点シェーダー内でボーン行列配列のインデックスとして使用されます。
ボーンインデックスの格納方法には主に8bit格納と32bit格納の2種類があります。
どちらを選ぶかはメモリ効率と処理のしやすさのトレードオフになります。
8bit格納と32bit格納のトレードオフ
- 8bit格納
ボーンインデックスを1頂点あたり4つの8bit(1バイト)で格納します。
これは通常DWORD
型の4バイトに4つのボーンインデックスを詰め込む形です。
メモリ使用量が少なく済み、頂点バッファのサイズを抑えられます。
ただし、頂点シェーダー内での扱いがやや複雑です。
8bit単位で分解して整数に変換する必要があり、シェーダーコードが長くなりやすいです。
また、ボーンインデックスの最大値は255までに制限されます。
- 32bit格納
ボーンインデックスをfloat4
などの32bit単位で格納し、各成分に1つずつボーンインデックスを格納します。
シェーダー内でのアクセスが簡単で、コードがシンプルになります。
ただし、メモリ使用量が増え、頂点バッファのサイズが大きくなります。
大量の頂点を扱う場合はパフォーマンスに影響する可能性があります。
項目 | 8bit格納 | 32bit格納 |
---|---|---|
メモリ使用量 | 少ない(4バイト/頂点) | 多い(16バイト/頂点) |
シェーダーの複雑さ | 高い(ビット操作が必要) | 低い(直接アクセス可能) |
ボーン数の最大値 | 255 | 実質的に制限なし |
パフォーマンス | メモリ帯域幅節約で有利 | メモリ負荷増加で不利 |
用途や環境に応じて使い分けるのが一般的です。
例えば、ボーン数が少なくメモリ節約を優先する場合は8bit格納、開発の手間やシェーダーの簡潔さを優先する場合は32bit格納が選ばれます。
ウェイト正規化のポイント
頂点に割り当てられたボーンウェイトは、合計が1.0になるように正規化されている必要があります。
正規化されていないと、頂点の変形が不正確になり、メッシュの破綻や不自然な動きが発生します。
ウェイト正規化のポイントは以下の通りです。
- 合計値の計算
すべてのウェイトを足し合わせます。
合計が0の場合は均等割り当てやデフォルト値を設定します。
- 正規化処理
各ウェイトを合計値で割り、合計が1.0になるように調整します。
- 小さいウェイトの切り捨て
パフォーマンス向上のため、一定以下の小さなウェイトは切り捨てることがあります。
その場合は再度正規化が必要です。
- 頂点シェーダー内での正規化
可能な限りCPU側で正規化を済ませるのが望ましいですが、シェーダー内での微調整も行うことがあります。
以下はCPU側でのウェイト正規化の例です。
#include <iostream>
#include <array>
void NormalizeWeights(float weights[], int count) {
float sum = 0.0f;
for (int i = 0; i < count; ++i) {
sum += weights[i];
}
if (sum == 0.0f) {
float equalWeight = 1.0f / count;
for (int i = 0; i < count; ++i) {
weights[i] = equalWeight;
}
} else {
for (int i = 0; i < count; ++i) {
weights[i] /= sum;
}
}
}
int main() {
std::array<float, 4> weights = {0.2f, 0.3f, 0.1f, 0.0f};
NormalizeWeights(weights.data(), 4);
for (float w : weights) {
std::cout << w << " ";
}
std::cout << std::endl;
return 0;
}
0.333333 0.5 0.166667 0
このコードでは、合計が0.6だったウェイトを正規化し、合計が1.0になるように調整しています。
Vertex Declaration設定例
DirectX9で頂点データを扱う際は、D3DVERTEXELEMENT9
構造体を使って頂点宣言(Vertex Declaration)を設定します。
スキニング用の頂点データには、位置、法線、テクスチャ座標、ボーンインデックス、ボーンウェイトなどが含まれます。
以下は、ボーンインデックスを8bit格納、ウェイトをfloat4で格納する例です。
#include <d3d9.h>
D3DVERTEXELEMENT9 vertexElements[] = {
{0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, // 位置
{0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0}, // 法線
{0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0}, // テクスチャ座標
{0, 32, D3DDECLTYPE_UBYTE4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BLENDINDICES, 0}, // ボーンインデックス(8bit x4)
{0, 36, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BLENDWEIGHT, 0}, // ボーンウェイト(float4)
D3DDECL_END()
};
この宣言では、頂点バッファのオフセットをバイト単位で指定し、各要素の型と用途を明示しています。
UBYTE4
は4つの8bit符号なし整数を格納し、ボーンインデックスとして使います。
ウェイトはFLOAT4
で4つの浮動小数点値を格納します。
Vertex Declarationは頂点シェーダーの入力レイアウトと一致させる必要があり、正しく設定しないと描画エラーや不正な変形が発生します。
インデックスバッファ再利用のヒント
スキニングでは、複数のメッシュやアニメーションで同じジオメトリ構造を使うことが多いため、インデックスバッファの再利用がパフォーマンス向上に繋がります。
- 共通ジオメトリの共有
同じメッシュ構造を持つキャラクターやLOD(レベル・オブ・ディテール)間でインデックスバッファを共有すると、メモリ使用量を削減できます。
- 頂点バッファの差分更新
スキニングは頂点の位置を変形するため、頂点バッファは動的に更新されることが多いですが、インデックスバッファは静的で変更不要です。
これにより、インデックスバッファは一度作成すれば複数フレームで使い回せます。
- 複数メッシュのバッチング
複数のメッシュを一つの大きな頂点・インデックスバッファにまとめることで、描画コールを減らせます。
インデックスバッファのオフセットを管理し、描画時に適切に指定します。
- インデックスフォーマットの選択
頂点数が65535以下なら16bitインデックスD3DFMT_INDEX16
を使い、メモリ節約とパフォーマンス向上を図ります。
頂点数が多い場合は32bitインデックスD3DFMT_INDEX32
を使います。
これらの工夫により、スキニング処理の効率化とメモリ管理の最適化が可能になります。
ボーン行列の計算ロジック
階層構造の更新アルゴリズム
ボーン行列の計算は、ボーンの階層構造を正しく反映させることが重要です。
各ボーンは親ボーンの変換を継承し、その上に自身のローカル変換を適用します。
これにより、親ボーンの動きが子ボーンに連動し、自然な関節の動きを実現します。
階層構造の更新は、一般的に以下のアルゴリズムで行います。
- ルートボーン(親が存在しないボーン)から処理を開始します。
- ルートボーンのワールド変換行列は、そのままローカル変換行列となります。
- 子ボーンは親ボーンのワールド変換行列に自身のローカル変換行列を掛け合わせてワールド変換行列を計算します。
- これを再帰的に全ボーンに対して行います。
この処理は深さ優先探索(DFS)や単純なループで実装可能です。
ボーン数が多い場合でも、親子関係を配列やツリー構造で管理し、効率的に更新します。
以下はC++での簡単な階層更新例です。
#include <vector>
#include <DirectXMath.h>
using namespace DirectX;
struct Bone {
int parentIndex;
XMMATRIX localTransform;
XMMATRIX worldTransform;
};
void UpdateBoneHierarchy(std::vector<Bone>& bones) {
for (size_t i = 0; i < bones.size(); ++i) {
if (bones[i].parentIndex < 0) {
bones[i].worldTransform = bones[i].localTransform;
} else {
bones[i].worldTransform = bones[bones[i].parentIndex].worldTransform * bones[i].localTransform;
}
}
}
このコードは親ボーンのワールド行列を使って子ボーンのワールド行列を計算しています。
親がいないボーンはローカル行列をそのままワールド行列とします。
親子行列合成の手順
親子行列合成は、ボーンのローカル変換行列を親ボーンのワールド変換行列に掛け合わせることで行います。
行列の掛け算は左から右へ順に適用されるため、以下のように計算します。
ここで、
は子ボーンのローカル変換行列(位置・回転・スケールを含む) は親ボーンのワールド変換行列 は子ボーンのワールド変換行列
この合成により、親ボーンの変換が子ボーンに正しく伝播します。
行列の掛け算は、DirectXMathのXMMatrixMultiply
関数などを使うと効率的です。
bones[childIndex].worldTransform = XMMatrixMultiply(bones[parentIndex].worldTransform, bones[childIndex].localTransform);
この手順を全ボーンに適用し、階層全体のワールド変換行列を更新します。
バインドポーズオフセット行列
バインドポーズオフセット行列(Inverse Bind Pose Matrix)は、スキニングで重要な役割を果たします。
これは、メッシュが初期状態(バインドポーズ)にあるときのボーンのワールド変換行列の逆行列です。
スキニング時には、頂点の初期位置をバインドポーズオフセット行列でボーン空間に変換し、その後アニメーションによるボーンの現在のワールド変換行列で変換します。
これにより、頂点が正しくボーンの動きに追従します。
計算式は以下の通りです。
は現在のボーンのワールド変換行列(アニメーション後) はバインドポーズの逆行列
この行列を頂点シェーダーに渡し、頂点変換に使用します。
バインドポーズオフセット行列はモデルのロード時に一度計算し、保存しておくのが一般的です。
計算例は以下の通りです。
XMMATRIX bindPoseMatrix = bones[i].worldTransform; // バインドポーズ時のワールド行列
XMMATRIX inverseBindPose = XMMatrixInverse(nullptr, bindPoseMatrix);
bones[i].offsetMatrix = inverseBindPose;
SIMD最適化での高速化
ボーン行列の計算は大量の行列演算を伴うため、CPUのSIMD(Single Instruction Multiple Data)命令を活用すると高速化が可能です。
DirectXMathライブラリはSIMD命令を内部で利用しており、効率的な行列計算をサポートしています。
SIMD最適化のポイントは以下の通りです。
- SIMD対応ライブラリの利用
DirectXMathやEigenなどのSIMD対応数学ライブラリを使うことで、行列・ベクトル演算が高速化されます。
- データのアライメント
SIMD命令は16バイト境界にアライメントされたデータに対して最適化されるため、ボーン行列や頂点データは16バイトアライメントで配置します。
- ループの展開とバッチ処理
ボーン行列の更新をループでまとめて処理し、SIMDレジスタに複数の行列データをロードして並列演算します。
- 不要な計算の削減
変化のないボーンは更新をスキップするなど、計算量を減らす工夫も重要です。
以下はDirectXMathを使ったSIMD対応の行列掛け算例です。
#include <DirectXMath.h>
using namespace DirectX;
void MultiplyBoneMatricesSIMD(const XMMATRIX* parents, const XMMATRIX* locals, XMMATRIX* worlds, size_t count) {
for (size_t i = 0; i < count; ++i) {
worlds[i] = XMMatrixMultiply(parents[i], locals[i]);
}
}
この関数はSIMD命令を内部で利用し、高速に行列の掛け算を行います。
ボーン行列の更新処理において、こうしたSIMD最適化を取り入れることで、リアルタイムアニメーションのパフォーマンス向上に繋がります。
頂点シェーダ実装
入力レイアウトの定義
DirectX9の頂点シェーダーでスキニングを実装する際、まずは頂点データの入力レイアウト(Input Layout)を正しく定義する必要があります。
入力レイアウトは、頂点バッファ内の各要素がどのようなデータ型で、どの用途(POSITION、NORMAL、BLENDWEIGHTなど)に対応しているかを示します。
スキニングに必要な主な頂点属性は以下の通りです。
- Position(位置): 頂点の3D座標。通常は
float3
またはfloat4
で格納 - BlendWeights(ボーンウェイト): 各頂点に影響を与えるボーンの重みです。
float4
で4つのウェイトを格納することが多いです - BlendIndices(ボーンインデックス): 影響を与えるボーンのインデックス。
ubyte4
やfloat4
で格納します - Normal(法線): ライティング計算に必要な法線ベクトル
- TexCoord(テクスチャ座標): UVマッピング用の2D座標
- Color(頂点カラー): 必要に応じて
以下は、HLSLの頂点シェーダー入力構造体の例です。
struct VS_INPUT
{
float4 Position : POSITION; // 頂点位置
float4 BlendWeights : BLENDWEIGHT; // ボーンウェイト
float4 BlendIndices : BLENDINDICES; // ボーンインデックス
float3 Normal : NORMAL; // 法線
float2 TexCoord : TEXCOORD0; // テクスチャ座標
};
このように、各頂点属性に対してセマンティクスを指定し、頂点シェーダーに正しくデータが渡るようにします。
頂点バッファのVertex Declarationと一致させることが重要です。
ボーン行列配列の定数転送
スキニングでは、CPU側で計算したボーンの変換行列を頂点シェーダーに渡し、頂点の変形に利用します。
DirectX9の頂点シェーダーでは、ボーン行列は定数レジスタ(constant registers)に配列として転送します。
ボーン行列は通常4×3行列(12要素)として格納し、1ボーンあたり3つのfloat4
定数レジスタを使用します。
例えば、最大32ボーンを扱う場合は96個の定数レジスタが必要です。
CPU側での転送例(DirectX9 C++)は以下の通りです。
const int MAX_BONES = 32;
D3DXMATRIX boneMatrices[MAX_BONES]; // ボーン行列配列
// ボーン行列をfloat4x3形式に変換して格納
float boneMatrixArray[MAX_BONES * 3][4];
for (int i = 0; i < MAX_BONES; ++i)
{
// D3DXMATRIXは4x4行列なので、4x3に変換
boneMatrixArray[i * 3 + 0][0] = boneMatrices[i]._11;
boneMatrixArray[i * 3 + 0][1] = boneMatrices[i]._12;
boneMatrixArray[i * 3 + 0][2] = boneMatrices[i]._13;
boneMatrixArray[i * 3 + 0][3] = boneMatrices[i]._14;
boneMatrixArray[i * 3 + 1][0] = boneMatrices[i]._21;
boneMatrixArray[i * 3 + 1][1] = boneMatrices[i]._22;
boneMatrixArray[i * 3 + 1][2] = boneMatrices[i]._23;
boneMatrixArray[i * 3 + 1][3] = boneMatrices[i]._24;
boneMatrixArray[i * 3 + 2][0] = boneMatrices[i]._31;
boneMatrixArray[i * 3 + 2][1] = boneMatrices[i]._32;
boneMatrixArray[i * 3 + 2][2] = boneMatrices[i]._33;
boneMatrixArray[i * 3 + 2][3] = boneMatrices[i]._34;
}
// シェーダーに定数配列をセット
pDevice->SetVertexShaderConstantF(0, &boneMatrixArray[0][0], MAX_BONES * 3);
頂点シェーダー側では、mWorldMatrixArray
のような配列として受け取り、ボーンインデックスを使ってアクセスします。
ウェイト付き位置変換
頂点の位置変換は、各ボーンの変換行列に頂点位置を乗算し、ボーンウェイトで加重平均することで行います。
これにより、複数のボーンの影響を受けた頂点の滑らかな変形が実現します。
HLSLでの基本的な計算例は以下の通りです。
float3 skinnedPosition = float3(0, 0, 0);
float totalWeight = 0.0f;
for (int i = 0; i < NUM_BONES; ++i)
{
int boneIndex = (int)input.BlendIndices[i];
float weight = input.BlendWeights[i];
float4x3 boneMatrix = mWorldMatrixArray[boneIndex];
// 位置を変換(4x3行列 × float4(位置,1))
float3 transformedPos = mul(input.Position, boneMatrix);
skinnedPosition += transformedPos * weight;
totalWeight += weight;
}
// ウェイトの合計が1でない場合は正規化
if (totalWeight > 0)
{
skinnedPosition /= totalWeight;
}
この処理により、頂点は複数ボーンの影響を受けて変形します。
mul
関数は4×3行列と4成分ベクトルの乗算を行い、効率的に計算できます。
法線・接ベクトルの変換処理
法線や接ベクトルも頂点の向きを決定する重要な属性であり、スキニング時に正しく変換する必要があります。
法線は位置と同様にボーン行列で変換しますが、平行移動成分は無視し、回転・スケールのみを適用します。
具体的には、4×3行列の上3×3部分(回転・スケール)を使って法線を変換し、その後正規化します。
HLSLでの法線変換例は以下の通りです。
float3 skinnedNormal = float3(0, 0, 0);
for (int i = 0; i < NUM_BONES; ++i)
{
int boneIndex = (int)input.BlendIndices[i];
float weight = input.BlendWeights[i];
float4x3 boneMatrix = mWorldMatrixArray[boneIndex];
// 3x3部分で法線を変換
float3 transformedNormal = mul(input.Normal, (float3x3)boneMatrix);
skinnedNormal += transformedNormal * weight;
}
skinnedNormal = normalize(skinnedNormal);
法線の正規化は必須で、ライティング計算の精度を保ちます。
接ベクトル(タンジェント)も同様に変換します。
追加属性の扱い(色・テクスチャ座標)
スキニング処理では、位置や法線以外の頂点属性も扱うことがあります。
代表的なものは頂点カラーやテクスチャ座標です。
- 頂点カラー
頂点カラーは通常、スキニングの影響を受けません。
したがって、頂点シェーダー内でそのまま出力に渡します。
- テクスチャ座標
テクスチャ座標も変形されない属性なので、スキニング処理の影響を受けずにそのままパススルーします。
これらの属性は頂点シェーダーの入力構造体に含め、出力構造体にそのままコピーします。
output.TexCoord = input.TexCoord;
output.DiffuseColor = input.Color;
このように、スキニングの計算対象外の属性は変換せずに扱うことで、処理を効率化しつつ描画に必要な情報を保持します。
ピクセルシェーダでの考慮
ライティング前後のスキニング差異
スキニング処理は通常、頂点シェーダーで頂点の位置や法線を変換してから、ピクセルシェーダーでライティング計算を行います。
この流れは「ライティング前スキニング」と呼ばれ、最も一般的な手法です。
一方で、スキニングをピクセルシェーダーで行う「ライティング後スキニング」という手法もありますが、DirectX9環境ではパフォーマンスや実装の複雑さからあまり使われません。
ライティング前スキニングの特徴
- 法線の正確な変換
頂点シェーダーで法線をボーン行列に基づいて変換し、正規化した状態でピクセルシェーダーに渡すため、ライティング計算が正確に行えます。
- パフォーマンス効率
頂点数はピクセル数より圧倒的に少ないため、スキニングを頂点シェーダーで行うほうが計算負荷が低くなります。
- シェーダーの分離
スキニング処理とライティング処理を明確に分けられるため、シェーダーの設計がシンプルになります。
ライティング後スキニングの特徴
- 高精度な変形
ピクセル単位でスキニングを行うため、頂点補間による変形誤差を減らせます。
- パフォーマンスコスト増大
ピクセル数分のスキニング計算が必要になるため、負荷が非常に高くなります。
- DirectX9では非推奨
Shader Model 2.0の制約やハードウェア性能の限界から、実用的ではありません。
まとめると、DirectX9環境では頂点シェーダーでスキニングを行い、ピクセルシェーダーでは変換済みの法線を使ってライティングを行う「ライティング前スキニング」が標準的です。
半透明マテリアル対応時の注意点
半透明マテリアル(アルファブレンディングを使うマテリアル)を扱う場合、スキニングと描画順序に注意が必要です。
半透明オブジェクトは描画順序によって見た目が大きく変わるため、正しい描画順序を保つことが重要です。
注意点1: 描画順序の管理
- 後ろから前への描画
半透明オブジェクトはカメラから遠い順に描画しないと、正しいアルファブレンド結果が得られません。
- スキニング後の頂点位置を使ったソート
スキニングで頂点位置が変化するため、描画順序のソートはスキニング後のワールド空間の位置を基準に行う必要があります。
注意点2: 深度バッファの扱い
- 深度書き込みの無効化
半透明描画時は深度バッファへの書き込みを無効にし、深度テストのみ有効にすることが多いです。
これにより、後続のオブジェクトが正しく描画されます。
- 深度ソートの補助
深度バッファだけでは正確な半透明表現が難しいため、描画順序の管理が必須です。
注意点3: シェーダー内のアルファ処理
- アルファ値の正確な計算
ピクセルシェーダーでアルファ値を正しく計算し、ブレンディングに反映させます。
スキニングによる変形でテクスチャ座標が変わる場合は注意が必要です。
- プリマルチプライドアルファの利用
ブレンディングの安定性向上のため、プリマルチプライドアルファ(色成分にアルファを乗算済み)を使うことが推奨されます。
半透明マテリアルをスキニングメッシュで扱う場合は、スキニング後の頂点位置を基に描画順序を管理し、深度バッファの設定やアルファ計算を適切に行うことが重要です。
これにより、透過表現の破綻を防ぎ、自然な見た目を実現できます。
パフォーマンス最適化
定数レジスタ節約テクニック
DirectX9の頂点シェーダーでは、ボーン行列を定数レジスタに格納してスキニングを行いますが、定数レジスタの数は限られており(Shader Model 2.0では最大96個程度)、これがパフォーマンスのボトルネックになることがあります。
そこで、定数レジスタの使用を節約するテクニックが重要です。
4×3行列の利用
ボーン行列は通常4×4行列ですが、スキニングでは平行移動成分を含む4×3行列(3列目まで)で十分です。
4×3行列は1ボーンあたり3つのfloat4
定数レジスタを使い、4×4行列の4つより1つ節約できます。
行列の圧縮と展開
ボーン行列の一部を圧縮し、頂点シェーダー内で展開する方法もあります。
例えば、回転をクォータニオンで格納し、シェーダーで行列に変換する手法です。
ただし、シェーダーの命令数が増えるためトレードオフがあります。
ボーン数の制限
1パスで扱うボーン数を制限し、必要なボーンだけを送ることで定数レジスタの使用を抑えます。
これにより、シェーダーの命令数や定数使用量を最適化できます。
定数バッファの再利用
複数のメッシュやキャラクターで同じボーン行列を共有できる場合は、定数バッファを再利用し、無駄な転送を減らします。
1パスあたりボーン数の分割戦略
DirectX9の頂点シェーダーは定数レジスタ数に制限があるため、1パスで処理できるボーン数は限られます。
多くのボーンを扱う場合は、複数パスに分割して描画する戦略が有効です。
分割の方法
- ボーングループごとにパスを分ける
ボーンを複数のグループに分割し、それぞれのグループに対応するボーン行列を1パスで送ります。
各パスで部分的にスキニングを行い、最終的に合成します。
- メッシュの分割
メッシュ自体をボーンの影響範囲で分割し、各部分を別パスで描画します。
これにより、1パスあたりのボーン数を抑えられます。
注意点
- 複数パス描画は描画コールが増えるため、オーバーヘッドに注意が必要です
- パス間で頂点の整合性を保つため、頂点バッファやインデックスバッファの管理が複雑になります
マトリックスパレット共有化
マトリックスパレットとは、スキニングで使用するボーン行列の配列のことです。
複数のメッシュやキャラクターで同じボーン構造を持つ場合、マトリックスパレットを共有することでメモリ使用量と転送コストを削減できます。
共有化のメリット
- メモリ節約
同じボーン行列を複数回保持する必要がなくなります。
- 転送効率向上
定数レジスタへのボーン行列転送を1回にまとめられ、GPUへの負荷が軽減されます。
実装例
- ボーン構造が同じ複数キャラクターのボーン行列を1つの配列にまとめる
- 描画時に共通のマトリックスパレットをセットし、各キャラクターの頂点シェーダーで参照
ただし、アニメーションが異なる場合はボーン行列が異なるため、共有化はアニメーションが共通のケースに限定されます。
ハードウェアインスタンシング併用
ハードウェアインスタンシングは、同じメッシュを複数回描画する際に頂点データを共有しつつ、インスタンスごとに異なる変換を適用できる機能です。
スキニングと組み合わせることで、複数キャラクターの描画効率を大幅に向上させられます。
インスタンシングの活用例
- 共通メッシュの複数キャラクター描画
同じスキンメッシュを持つ複数キャラクターをインスタンスとして描画し、ボーン行列やアニメーションパラメータをインスタンスごとに渡します。
- ボーン行列のインスタンス定数として渡す
インスタンスごとに異なるボーン行列配列を頂点シェーダーに渡し、1回の描画コールで複数キャラクターを処理。
メリット
- 描画コール数を削減し、CPU負荷を軽減
- GPUのパイプライン効率が向上
実装上の注意点
- DirectX9ではインスタンシングのサポートが限定的なため、拡張機能やカスタムシェーダーが必要になる場合があります
- ボーン行列の定数転送量が増えるため、定数レジスタの制限に注意が必要です
これらのパフォーマンス最適化を組み合わせることで、DirectX9環境でも効率的かつ高品質なスキニングアニメーションを実現できます。
メモリ効率化
ウェイト値の圧縮手法
スキニングにおいて、各頂点に割り当てられるボーンウェイトは通常float
型で格納されますが、これが大量の頂点に対して蓄積されるとメモリ使用量が大きくなります。
ウェイト値の圧縮はメモリ効率化に効果的です。
8bit整数への量子化
ウェイト値は0.0から1.0の範囲にあるため、float
(32bit)からuint8_t
(8bit)に量子化することで4分の1のメモリに削減できます。
量子化の際は、ウェイトを255倍して整数化し、シェーダー内で再度正規化します。
uint8_t quantizeWeight(float weight) {
return static_cast<uint8_t>(weight * 255.0f + 0.5f);
}
float dequantizeWeight(uint8_t qWeight) {
return qWeight / 255.0f;
}
ウェイトの合計を1に保つ工夫
量子化誤差によりウェイトの合計が1.0からずれるため、最後のウェイトは残りの値を計算して補正する方法が一般的です。
3ウェイト+1補正ウェイト方式
4つのウェイトのうち3つだけを格納し、4つ目は「1.0 – (3つの合計)」として計算する方法もあります。
これにより、格納データを減らしつつ合計1.0を保証できます。
圧縮のメリットとデメリット
項目 | メリット | デメリット |
---|---|---|
メモリ使用量 | 75%削減(32bit→8bitの場合) | 量子化誤差による精度低下 |
転送帯域幅 | 転送データ量が減少 | シェーダー内での復元処理が必要 |
実装の複雑さ | 中程度 | シェーダーコードがやや複雑化 |
インデックスレンジ再構成
インデックスバッファは頂点の参照を効率化しますが、頂点数が多いとインデックスのビット幅も増え、メモリ使用量が増加します。
インデックスレンジ再構成は、頂点インデックスの範囲を狭めてインデックスのビット幅を削減する手法です。
頂点の再配置
メッシュの頂点をグループ化し、使用される頂点インデックスの範囲を小さくします。
これにより、16bitインデックス(最大65535)で収まるように調整可能です。
インデックスのオフセット適用
各グループの頂点インデックスにオフセットを適用し、インデックス値を0から始まる連続した範囲に再構成します。
描画時にオフセットを考慮して頂点バッファを参照します。
メモリとパフォーマンスのバランス
インデックスレンジを狭めることで16bitインデックスを使えるようになり、メモリ使用量と転送帯域幅を削減できます。
ただし、頂点の再配置やグループ分けの処理が必要で、実装コストが増えます。
動的頂点バッファと静的頂点バッファの使い分け
頂点バッファはGPUに頂点データを渡すためのメモリ領域ですが、用途に応じて動的(Dynamic)と静的(Static)に使い分けることで効率的なメモリ管理が可能です。
静的頂点バッファ
- 用途: 変化しない頂点データ(例:静止メッシュ、変形しない部分)
- 特徴: 作成時にデータをセットし、その後は変更しない
- メリット: GPUメモリに最適化され、高速な描画が可能
- デメリット: 頻繁な更新には不向き
動的頂点バッファ
- 用途: 毎フレームや頻繁に頂点データが更新される場合(例:スキニング後の頂点位置)
- 特徴: CPUから頻繁に書き込み可能で、GPUに効率的に転送される
- メリット: アニメーションや変形に対応しやすい
- デメリット: 静的バッファより描画性能が若干低下する可能性がある
使い分けのポイント
- スキニング頂点の位置や法線など、毎フレーム変わるデータは動的頂点バッファに格納し、CPUで更新してGPUに転送します
- テクスチャ座標やボーンインデックス、ウェイトなど変化しないデータは静的頂点バッファに格納し、更新コストを抑えます
実装例(DirectX9)
// 動的頂点バッファの作成例
pDevice->CreateVertexBuffer(
vertexCount * sizeof(Vertex),
D3DUSAGE_DYNAMIC | D3DUSAGE_WRITEONLY,
FVF_VERTEX,
D3DPOOL_DEFAULT,
&pDynamicVertexBuffer,
nullptr);
// 静的頂点バッファの作成例
pDevice->CreateVertexBuffer(
vertexCount * sizeof(Vertex),
0,
FVF_VERTEX,
D3DPOOL_MANAGED,
&pStaticVertexBuffer,
nullptr);
このように、用途に応じてバッファタイプを使い分けることで、メモリ使用量とパフォーマンスのバランスを最適化できます。
ハードウェア互換性チェック
Shader Model 2.0と3.0の差異
DirectX9環境でスキニングを実装する際、Shader Model(SM)2.0と3.0の違いはパフォーマンスや表現力に大きく影響します。
両者の主な差異を理解し、対応することで最適な実装が可能です。
定数レジスタ数の違い
- SM 2.0
頂点シェーダーの定数レジスタは最大96個(float4単位)まで使用可能です。
ボーン行列を4×3形式で格納すると、1ボーンあたり3レジスタ消費のため、理論上は最大約32ボーン扱えますが、他の定数も使うため実際はもっと少なくなります。
- SM 3.0
定数レジスタ数が増加し、より多くのボーン行列を扱えます。
また、動的ループや条件分岐がサポートされ、柔軟なシェーダー記述が可能です。
ループと条件分岐のサポート
- SM 2.0
ループは静的展開が必須で、動的なループや条件分岐は制限されます。
ボーン数を固定し、ループアンローリング(展開)する必要があります。
- SM 3.0
動的ループや条件分岐が可能で、ボーン数を柔軟に扱えます。
これにより、複雑なスキニングやアニメーション制御が容易になります。
命令数制限
- SM 2.0
頂点シェーダーの命令数は最大64命令まで。
複雑なスキニング処理では命令数オーバーに注意が必要です。
- SM 3.0
命令数制限が大幅に緩和され、より複雑な処理が可能です。
テクスチャサンプリング
- SM 2.0
頂点シェーダーでのテクスチャサンプリングは不可。
ボーン行列は定数レジスタで渡す必要があります。
- SM 3.0
頂点シェーダーでテクスチャサンプリングが可能です。
ボーン行列をテクスチャに格納し、柔軟に参照できます。
これらの違いにより、SM 3.0はより高品質で柔軟なスキニング実装が可能ですが、SM 2.0はハードウェア互換性が高く、古いGPUでも動作します。
ベンダー別最適化(ATIとNVIDIA)
DirectX9時代のGPUは主にATI(現AMD)とNVIDIAの2大ベンダーが市場を占めており、それぞれのGPUアーキテクチャに最適化したスキニング実装がパフォーマンス向上に繋がります。
ATI(AMD)GPUの特徴
- 定数レジスタの扱い
ATIのGPUは定数レジスタのアクセスに若干のレイテンシがあるため、定数の使用を最小限に抑えることが重要です。
- 命令スケジューリング
命令の並列実行が得意なため、命令数を減らすよりも命令の依存関係を減らす工夫が効果的です。
- ループ展開の影響
ループ展開が多すぎるとパフォーマンスが低下する傾向があるため、適度な展開が望ましいです。
NVIDIA GPUの特徴
- 定数レジスタの高速アクセス
NVIDIAのGPUは定数レジスタへのアクセスが高速で、定数数が多くても比較的パフォーマンスが安定します。
- 命令数制限の影響
命令数が多いとパフォーマンスが低下しやすいため、命令数削減が重要です。
- ループ展開の効果
ループ展開によりパフォーマンスが向上する場合が多く、積極的な展開が推奨されます。
最適化のポイント
項目 | ATI(AMD)GPU | NVIDIA GPU |
---|---|---|
定数レジスタ使用量 | 最小限に抑える | 多めでも許容 |
命令数 | 依存関係を減らす | 命令数削減が重要 |
ループ展開 | 適度に抑える | 積極的に展開 |
これらの違いを考慮し、シェーダーコードをベンダー別に調整することで、両者で安定したパフォーマンスを得られます。
モバイルGPUでの制限事項
モバイルGPUはデスクトップGPUに比べて性能や機能に制限が多く、DirectX9スキニングの実装においても注意が必要です。
主な制限事項
- Shader Modelの制限
多くのモバイルGPUはShader Model 2.0以下のサポートに留まり、命令数や定数レジスタ数が非常に限られています。
- 定数レジスタ数の制限
定数レジスタ数が少なく、ボーン行列を多く渡せないため、1頂点あたりのボーン数を減らす必要があります。
- 命令数制限
命令数の上限が低く、複雑なスキニング処理は実装困難です。
- テクスチャサンプリング不可
頂点シェーダーでのテクスチャサンプリングが使えないため、ボーン行列は定数レジスタで渡す必要があります。
- パフォーマンス制約
CPU・GPUともに性能が低いため、スキニング処理はできるだけ軽量化し、頂点数やボーン数を抑えることが重要です。
対策例
- ボーン数の削減
1頂点あたりのボーン数を2~4に制限し、必要最低限のボーンのみを使用。
- LOD(レベル・オブ・ディテール)の導入
遠距離ではボーン数や頂点数を減らし、負荷を軽減。
- CPUスキニングの併用
GPUでのスキニングが困難な場合は、CPU側でスキニングを行い、変形済み頂点をGPUに送る方法もあります。
- シェーダーの最適化
命令数を削減し、条件分岐やループを極力避けます。
モバイルGPUの制限を踏まえた設計は、安定した動作と快適なパフォーマンスを実現するために不可欠です。
デバッグと可視化
GPUデバッガ活用例
DirectX9でスキニングを実装する際、GPU側の処理はブラックボックスになりがちで、問題の特定が難しいことがあります。
GPUデバッガを活用することで、頂点シェーダーやピクセルシェーダーの動作を詳細に解析し、バグの原因を特定しやすくなります。
代表的なGPUデバッガには、MicrosoftのVisual Studio Graphics DebuggerやNVIDIA Nsight、RenderDocなどがあります。
これらのツールを使うと、以下のようなことが可能です。
- シェーダーステージのステップ実行
頂点シェーダーの各命令をステップ実行し、レジスタの値や定数の内容を確認できます。
スキニング行列の適用結果やウェイトの計算過程を追跡可能です。
- 頂点データの可視化
頂点バッファの内容や変換後の頂点位置を確認し、スキニングの結果が期待通りか検証できます。
- レンダリングパイプラインの解析
入力から出力までのパイプラインを追い、どの段階で問題が発生しているかを特定します。
- シェーダーのパフォーマンス分析
命令数や実行時間を計測し、ボトルネックを発見できます。
これらの機能を活用し、スキニング処理の不具合やパフォーマンス問題を効率的に解決しましょう。
Bone Weight表示による確認
スキニングの品質を確認するために、各頂点のボーンウェイトを可視化する手法があります。
ウェイトの割り当てが適切でないと、関節部分のメッシュが不自然に変形するため、ウェイトの分布を視覚的に把握することは重要です。
実装例
- 頂点カラーへのウェイト反映
頂点カラーのRGB成分にボーンウェイトを割り当て、特定のボーンの影響度を色で表現します。
例えば、赤成分にボーン0のウェイト、緑成分にボーン1のウェイト、青成分にボーン2のウェイトを割り当てる方法です。
- シェーダーでのカラー出力
頂点シェーダーやピクセルシェーダーでウェイトをカラーに変換し、画面に表示します。
float3 weightColor = float3(input.BlendWeights[0], input.BlendWeights[1], input.BlendWeights[2]);
output.Color = float4(weightColor, 1.0f);
効果
- ウェイトが均等に割り当てられているか確認できます
- 不自然なウェイト割り当てや切り捨てミスを発見しやすくなります
- モデルの修正やウェイト調整の指針になります
行列破綻のトラブルシュート
スキニングでボーン行列が正しく計算されていない場合、メッシュが異常に変形したり、破綻したりすることがあります。
こうした問題を解決するためのトラブルシュート手法を紹介します。
行列の初期化確認
- 行列が未初期化のまま使用されていないか確認します
- バインドポーズオフセット行列の逆行列計算が正しく行われているかチェックします
階層構造の親子関係確認
- ボーンの親子インデックスが正しく設定されているか
- 親ボーンの行列が正しく更新されているか
行列の正規性チェック
- 行列のスケールや回転成分が異常値(NaNやInf)になっていないか
- 行列の逆行列計算でエラーが発生していないか
デバッグ用出力
- ボーン行列の値をログに出力し、異常な値を検出
- GPUデバッガで行列の内容を確認
シェーダー内の計算確認
- ボーンインデックスやウェイトの範囲外アクセスがないか
- ウェイトの合計が1.0に正規化されているか
これらのチェックを順に行うことで、行列破綻の原因を特定しやすくなります。
パフォーマンスカウンター利用法
スキニング処理のパフォーマンスを最適化するために、GPUやCPUのパフォーマンスカウンターを活用します。
これにより、どの処理がボトルネックになっているかを定量的に把握できます。
GPUパフォーマンスカウンター
- GPU時間計測
スキニングを含む描画パスのGPU処理時間を計測し、負荷の高い部分を特定します。
- シェーダー命令数
頂点シェーダーの命令数やレジスタ使用量を計測し、最適化の余地を探ります。
- メモリ帯域幅
頂点バッファや定数バッファの転送量を監視し、メモリボトルネックを検出します。
CPUパフォーマンスカウンター
- CPU時間計測
ボーン行列計算や頂点データの更新にかかるCPU時間を計測し、効率化を検討します。
- スレッド負荷
マルチスレッド環境での負荷分散状況を把握し、スレッド間のバランスを調整します。
ツール例
- Visual Studio Graphics Analyzer
GPUのパフォーマンス解析に利用可能です。
- PIX for Windows
DirectXアプリケーションの詳細なパフォーマンス解析が可能です。
- GPUベンダー提供ツール
NVIDIA Nsight、AMD Radeon GPU Profilerなど。
活用ポイント
- パフォーマンスカウンターの結果を元に、定数レジスタの使用削減や頂点バッファの最適化を行います
- ボーン数や頂点数の調整、シェーダー命令の削減を検討
- CPUとGPUの負荷バランスを見て、処理の分散や最適化を図ります
これらの手法を駆使して、スキニング処理のパフォーマンスを継続的に改善していくことが重要です。
アニメーションデータ取り込み
FBXエクスポート時の留意点
FBXは3Dモデルやアニメーションデータの交換フォーマットとして広く使われていますが、DirectX9のスキニング実装に適した形でデータを取り込むためには、エクスポート時にいくつかのポイントに注意が必要です。
ボーン階層の正確な保持
FBXエクスポート時にボーンの親子関係が正しく保持されていることを確認します。
階層構造が崩れると、スキニング時の行列計算が誤り、メッシュの変形が不自然になります。
バインドポーズのエクスポート
バインドポーズ(初期姿勢)のボーン行列が正しくエクスポートされていることが重要です。
これにより、オフセット行列の計算や逆行列の取得が正確に行えます。
ウェイトとボーンインデックスの制限
FBXのウェイト情報は頂点ごとに複数のボーンが割り当てられますが、DirectX9の頂点シェーダーでは1頂点あたり最大4ボーンが一般的です。
エクスポート時にウェイト数を制限し、不要なウェイトは切り捨てるか統合する設定を行うと良いでしょう。
アニメーションのフレームレートと範囲
アニメーションのフレームレートや開始・終了フレームを適切に設定し、不要なフレームを含めないようにします。
これにより、データサイズの削減と再生の安定化が図れます。
スケールと単位の統一
FBXファイルのスケールや単位設定がプロジェクトの基準と一致しているか確認します。
単位の不一致はボーンの位置やアニメーションの動きに影響を与えます。
カスタムフォーマット設計指針
DirectX9で効率的にスキニングアニメーションを扱うために、FBXなどの汎用フォーマットから変換したカスタムフォーマットを設計することが多いです。
以下の指針を参考に設計します。
データの最小化と高速アクセス
- 必要な情報のみを格納し、不要なメタデータは省きます
- 頂点ごとのボーンインデックスとウェイトは固定長(例:4つ)に統一し、アクセスを高速化
- 行列やクォータニオンは圧縮や量子化を検討し、メモリ使用量を削減
階層構造の明示的な管理
- ボーンの親子関係を明示的に格納し、CPU側での階層更新を容易にします
- ボーン名やIDを保持し、デバッグやツール連携をサポート
アニメーションデータの分割
- キーフレームデータはボーンごとに分割し、必要なボーンだけを読み込めるようにします
- 複数のアニメーション(歩行、走行、攻撃など)を別々に管理し、切り替えを容易に
補間方式の指定
- キーフレームの補間方法(線形、スプラインなど)を明示的に指定し、再生時に適切な補間を行います
バージョニングと互換性
- フォーマットにバージョン情報を持たせ、将来的な拡張や互換性維持を考慮
キーフレームとスプライン補間の使い分け
アニメーションの滑らかさや表現力を高めるために、キーフレーム補間には主に「線形補間」と「スプライン補間」が使われます。
用途に応じて使い分けることが重要です。
線形補間(Linear Interpolation)
- キーフレーム間を直線的に補間する方法
- 計算が軽く、実装が簡単
- 動きが直線的で不自然になる場合があるが、単純な動作や高速な処理が求められる場合に適します
スプライン補間(Spline Interpolation)
- キーフレーム間を曲線(ベジェ曲線やカーディナルスプラインなど)で補間する方法
- 動きが滑らかで自然に見えます
- 計算コストが高く、実装も複雑
- 表情や関節の微妙な動き、キャラクターの自然な動作に適しています
使い分けの例
シナリオ | 推奨補間方式 | 理由 |
---|---|---|
基本的な移動や回転 | 線形補間 | 軽量で十分な表現力 |
表情アニメーション | スプライン補間 | 滑らかで自然な変化が必要 |
複雑な関節の動き | スプライン補間 | 不自然な動きを防止 |
リアルタイム処理重視 | 線形補間 | 計算負荷を抑え、パフォーマンス優先 |
実装上の注意点
- スプライン補間ではキーフレームの前後の制御点が必要なため、データ構造を工夫します
- 線形補間とスプライン補間を混在させる場合は、補間方式を明示的に管理します
- 補間結果の正規化(特に回転クォータニオン)を忘れない
これらを踏まえ、アニメーションデータの取り込みと補間処理を設計することで、DirectX9環境でも高品質なスキニングアニメーションを実現できます。
複数キャラクター同時描画
描画バッチング戦略
複数のキャラクターを同時に描画する際、描画コール数(Draw Call)を減らすことがパフォーマンス向上の鍵となります。
描画バッチングは、複数のメッシュやキャラクターをまとめて一度の描画コールで処理する技術です。
インスタンシングによるバッチング
同じメッシュを持つ複数キャラクターをインスタンシングで描画します。
インスタンスごとに異なるボーン行列や変換行列を頂点シェーダーに渡し、1回のDrawCallで複数キャラクターを描画可能です。
- メリット
描画コール削減によるCPU負荷軽減。
GPU側での並列処理効率向上。
- 実装ポイント
ボーン行列配列をインスタンスごとに管理し、頂点シェーダーで参照。
DirectX9ではインスタンシングのサポートが限定的なため、拡張機能やカスタムシェーダーが必要でしょう。
メッシュの統合バッチング
複数のキャラクターのメッシュを一つの大きな頂点バッファ・インデックスバッファにまとめ、描画時にオフセットを指定して描画します。
- メリット
頂点バッファ切り替えのオーバーヘッド削減。
頂点データの連続性によるキャッシュ効率向上。
- 注意点
メッシュの頂点属性やマテリアルが異なる場合は分割が必要でしょう。
ボーン行列の管理が複雑になることがあります。
マテリアル・シェーダー単位でのバッチング
同じマテリアルやシェーダーを使うキャラクターをまとめて描画し、状態変更を減らします。
これにより、レンダリングパイプラインの切り替えコストを削減できます。
マルチスレッド分割
複数キャラクターのスキニングや描画処理はCPU負荷が高いため、マルチスレッド化による負荷分散が効果的です。
アニメーション更新の並列化
- 各キャラクターのボーン行列計算やアニメーション更新を複数スレッドで並列処理
- スレッド間のデータ競合を避けるため、キャラクターごとに独立したデータ構造を用意
描画コマンド生成の分割
- 描画コマンドの生成やバッチング処理を複数スレッドで分担
- メインスレッドはGPUへの送信に専念し、CPUのボトルネックを軽減
スレッドプールの活用
- スレッドプールを使い、動的にスレッド数を調整
- 負荷に応じてスレッド数を増減し、リソースを効率的に利用
同期と排他制御
- データの整合性を保つため、適切な同期機構(ミューテックスやロックフリー構造)を導入
- 過剰な同期はパフォーマンス低下の原因となるため、最小限に抑えます
メッシュレベルLOD導入
複数キャラクターを効率的に描画するために、メッシュのレベル・オブ・ディテール(LOD)を導入し、描画負荷を動的に調整します。
LODの基本
- 距離や画面サイズに応じて、頂点数やボーン数の異なる複数のメッシュを切り替えます
- 遠距離では低詳細メッシュを使い、描画負荷を軽減
スキニング対応LOD
- LODごとにボーン数やウェイト数を調整し、スキニング計算の負荷も削減
- 低LODではボーン数を減らし、簡易的なスキニングを適用
LOD切り替えの工夫
- カメラ距離だけでなく、画面占有率やパフォーマンス状況を考慮して切り替え
- 切り替え時のポップアップ(急激な変化)を抑えるため、フェードや補間を導入
実装例
- 複数の頂点バッファとインデックスバッファを用意し、描画時に適切なLODを選択
- ボーン行列配列もLODに合わせて切り替え、必要なボーン数だけを送信
これらの手法を組み合わせることで、複数キャラクターの同時描画におけるCPU・GPU負荷を抑え、快適なリアルタイム描画を実現できます。
衝突判定と物理連携
ボーンベースのヒットテスト
スキニングキャラクターの衝突判定では、ボーン構造を活用したヒットテストが効果的です。
ボーンベースのヒットテストは、メッシュ全体ではなくボーン単位で衝突判定を行うため、計算負荷を抑えつつ精度の高い判定が可能です。
実装のポイント
- ボーンに対応したコリジョン形状の設定
各ボーンに対して球(Sphere)、カプセル(Capsule)、またはボックス(Box)などの単純なコリジョン形状を割り当てます。
これにより、複雑なメッシュ形状を単純化し、衝突判定を高速化します。
- ボーンのワールド変換を利用
ボーンのワールド変換行列を用いて、コリジョン形状の位置や向きを更新します。
これにより、キャラクターの動きに合わせて衝突判定領域が正確に追従します。
- ヒットテストの流れ
- レイキャストや他の衝突形状との交差判定をボーン単位で行います。
- 衝突が検出されたボーンを特定し、必要に応じて詳細なメッシュレベルの判定に切り替えます。
メリット
- 計算コストが低く、リアルタイム処理に適しています
- 衝突判定の精度をボーン単位で調整可能です
- 衝突箇所の特定が容易で、ダメージ判定やエフェクト制御に活用できます
ラグドールへの拡張
ラグドールは、キャラクターの骨格を物理シミュレーションで制御し、自然な倒れ込みや衝突反応を表現する技術です。
スキニングのボーン構造を活用してラグドールを実装することで、リアルな物理挙動を実現します。
ラグドールの基本構成
- 物理ボーンの作成
スキニングボーンに対応する物理ボーン(剛体)を作成し、関節部分にジョイント(ヒンジやスプリング)を設定します。
- 物理シミュレーションの適用
剛体とジョイントの物理挙動を物理エンジン(例:PhysX、Bullet)で計算し、ボーンの位置・回転を更新。
- スキニングとの連携
物理シミュレーション結果をスキニングのボーン行列に反映し、メッシュの変形に反映させます。
実装上の注意点
- 物理ボーンの質量や慣性テンソルを適切に設定し、安定した挙動を実現
- ジョイントの可動範囲を制限し、不自然な動きを防止
- アニメーションと物理シミュレーションの切り替え(例:倒れた瞬間にラグドールへ移行)を管理
フィジカルボーン追加手順
スキニングボーンに物理的な挙動を持たせるためのフィジカルボーン(物理ボーン)を追加する手順は以下の通りです。
ボーン構造の解析
- スキニングボーンの階層構造と名称を取得
- 物理シミュレーションに必要なボーンを選定(全ボーンでなくてもよい)
剛体(Rigid Body)の作成
- 選定したボーンごとに剛体を作成
- 剛体の形状は球、カプセル、ボックスなど単純形状で近似
- 剛体の質量や慣性を設定
ジョイントの設定
- 親子ボーン間にジョイントを設置し、可動範囲やバネ定数を設定
- ジョイントタイプはヒンジ、スプリング、ボールジョイントなど用途に応じて選択
物理シミュレーションとの連携
- 物理エンジンに剛体とジョイントを登録
- 毎フレームの物理計算結果をボーン行列に反映し、スキニングに利用
アニメーションとの同期
- 通常時はアニメーション駆動、必要に応じて物理駆動に切り替え
- 切り替え時のスムーズな遷移を実装
デバッグと調整
- 物理挙動を可視化し、剛体の形状やジョイントパラメータを調整
- 不自然な動きや破綻を防ぐための制約を追加
これらの手順を踏むことで、スキニングキャラクターにリアルな物理挙動を付加し、ゲームやシミュレーションの没入感を高められます。
ブレンドシェイプ併用
表情アニメーションの組み込み
表情アニメーションはキャラクターの感情表現や細かな顔の動きを実現するために重要で、ボーンスキニングだけでは表現しきれない微細な変形を補完します。
表情アニメーションの代表的な手法として「ブレンドシェイプ(モーフターゲット)」があり、これをスキニングと組み合わせて使うことが一般的です。
ブレンドシェイプの基本
- モーフターゲット
基本メッシュ(ベースメッシュ)に対して、特定の表情や形状変化を持つ複数のターゲットメッシュを用意します。
- 重み付け合成
各モーフターゲットの頂点変位を重み(0.0〜1.0)でスケーリングし、ベースメッシュに加算して最終形状を生成します。
表情アニメーションの組み込み手順
- モーフターゲットの準備
表情ごとに変形した頂点オフセットを用意し、頂点単位で差分を格納します。
- 重みの制御
アニメーションシステムやスクリプトで各表情の重みを制御し、複数の表情を同時にブレンド可能にします。
- 頂点変形の計算
CPUまたはGPUで、ベースメッシュの頂点位置にモーフターゲットのオフセットを重み付きで加算します。
- スキニングとの連携
ブレンドシェイプで変形した頂点をボーンスキニングの前後どちらかで処理します(後述)。
GPUでの実装例
- 頂点シェーダー内でのモーフターゲット加算
複数のモーフターゲット頂点オフセットを頂点シェーダーに渡し、重みを掛けて加算。
- テクスチャバッファ利用
モーフターゲットの頂点オフセットをテクスチャに格納し、頂点シェーダーでサンプリングする手法もあります。
ボーンスキニングとモーフターゲットの統合
ボーンスキニングとブレンドシェイプ(モーフターゲット)は、それぞれ異なる変形手法ですが、リアルなキャラクター表現には両者の統合が不可欠です。
統合方法には主に「モーフターゲット先行」と「スキニング先行」の2パターンがあります。
モーフターゲット先行(Morph-then-Skin)
- 処理順序
まずベースメッシュにモーフターゲットの頂点オフセットを加算し、変形した頂点をボーンスキニングに渡します。
- メリット
表情変形がボーン変形に自然に追従し、関節の動きに合わせて表情も変化。
- デメリット
モーフターゲットの頂点オフセットがボーン変形前の座標系である必要があり、データ準備がやや複雑。
スキニング先行(Skin-then-Morph)
- 処理順序
まずボーンスキニングで頂点を変形し、その後モーフターゲットのオフセットを加算します。
- メリット
モーフターゲットのデータ準備が簡単で、ボーン変形に依存しない。
- デメリット
関節の動きに表情が追従しにくく、不自然な変形になることがあります。
実装上の注意点
- 頂点座標系の統一
モーフターゲットのオフセットがどの座標系(ローカル、ボーン空間など)で定義されているかを明確にし、処理順序に合わせて変換する必要があります。
- パフォーマンス考慮
両者をGPUで処理する場合、頂点シェーダーの命令数が増加するため、最適化が重要です。
- ウェイトの正規化
スキニングのウェイトとモーフターゲットの重みを適切に管理し、合計が1.0になるように調整します。
サンプルコード(HLSLでのモーフ先行例)
// ベース頂点位置
float3 basePos = input.Position.xyz;
// モーフターゲットの頂点オフセット(複数ターゲットの加算)
float3 morphOffset = float3(0,0,0);
for (int i = 0; i < NUM_MORPHS; ++i)
{
morphOffset += morphWeights[i] * morphTargets[i];
}
// モーフ変形後の頂点位置
float3 morphedPos = basePos + morphOffset;
// ボーンスキニング処理(省略)
// skinnedPos = Skinning(morphedPos, ...);
このように、ブレンドシェイプとボーンスキニングを組み合わせることで、表情豊かで自然なキャラクターアニメーションを実現できます。
クロスプラットフォーム移植
DirectX10/11への変換ポイント
DirectX9からDirectX10/11へスキニング実装を移植する際には、APIの仕様変更や機能強化に対応する必要があります。
主な変換ポイントは以下の通りです。
シェーダーモデルの変更
- DirectX10/11ではシェーダーモデルが5.0に対応し、より高度なシェーダー機能が利用可能です
- 頂点シェーダーの定数レジスタ制限が緩和され、ボーン行列の管理が容易になります
- HLSLの構文や組み込み関数も拡張されているため、コードの最適化や簡素化が可能です
入力レイアウトの変更
- DirectX9の
D3DVERTEXELEMENT9
に代わり、DirectX10/11ではID3D11InputLayout
を使用します - 入力レイアウトの作成方法が異なり、HLSLの入力構造体と厳密に一致させる必要があります
定数バッファの導入
- DirectX10/11では定数バッファ(Constant Buffer)が導入され、複数の定数をまとめて効率的にGPUに転送可能です
- ボーン行列配列は定数バッファに格納し、更新頻度に応じて動的または静的に管理します
リソース管理の変更
- DirectX9の固定機能パイプラインは廃止され、すべてシェーダーベースのパイプラインに統一
- リソースの作成・バインド方法が変わるため、頂点バッファやインデックスバッファの管理を見直す必要があります
マルチスレッドレンダリング対応
- DirectX11はマルチスレッドレンダリングをサポートしており、複数スレッドでコマンドリストを生成可能です
- スキニング処理のCPU負荷分散や描画バッチングの最適化に活用できます
OpenGL Uniform Block対応策
OpenGLでDirectX9のスキニングを移植する際、Uniform Block(Uniform Buffer Object: UBO)を活用してボーン行列などの定数データを効率的に管理します。
Uniform Blockの概要
- Uniform Blockは複数のuniform変数をまとめてGPUに転送できるバッファで、DirectXの定数バッファに相当
- 大量のボーン行列を一括で管理し、描画時のバインド回数を削減可能です
ボーン行列の格納
- ボーン行列は
mat4
配列としてUniform Blockに格納 - GLSLの
std140
レイアウトに従い、メモリアライメントに注意して配置
layout(std140) uniform BoneMatrices {
mat4 boneMatrixArray[MAX_BONES];
};
バッファの作成と更新
- OpenGLの
glGenBuffers
、glBindBuffer
、glBufferData
でUniform Buffer Objectを作成 - CPU側でボーン行列を更新し、
glBufferSubData
やglMapBuffer
でGPUに転送
シェーダー側の利用
- 頂点シェーダーでUniform Blockのボーン行列を参照し、ボーンインデックスとウェイトを使ってスキニング計算を行います
注意点
- OpenGLのUniform Blockサイズ制限に注意し、必要に応じて複数のUniform Blockに分割
- OpenGL ES環境ではUniform Blockがサポートされていない場合があるため、代替手段(テクスチャバッファなど)を検討
HLSLからGLSLへのシェーダ変換
DirectX9のHLSLシェーダーをOpenGLのGLSLに移植する際は、言語仕様や組み込み関数の違いに注意が必要です。
シェーダーステージの対応
- HLSLの頂点シェーダーはGLSLの頂点シェーダーに対応
- セマンティクス(例:
POSITION
、NORMAL
)はGLSLでは使わず、layout(location = x)
で属性位置を指定
入力・出力変数の宣言
- HLSLの
struct
による入出力はGLSLではin
、out
キーワードで宣言
// HLSL
struct VS_INPUT {
float4 Position : POSITION;
float4 BlendWeights : BLENDWEIGHT;
float4 BlendIndices : BLENDINDICES;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD0;
};
// GLSL
layout(location = 0) in vec4 Position;
layout(location = 1) in vec4 BlendWeights;
layout(location = 2) in vec4 BlendIndices;
layout(location = 3) in vec3 Normal;
layout(location = 4) in vec2 TexCoord;
out vec2 vTexCoord;
定数バッファの置き換え
- HLSLの
uniform float4x3 mWorldMatrixArray[MAX_BONES];
はGLSLのUniform Blockや配列に置き換え
uniform mat4 boneMatrixArray[MAX_BONES];
関数や演算子の違い
mul(a, b)
はGLSLではb * a
の行列・ベクトル乗算に置き換え- 型変換やスワズル(例:
.xyz
)はGLSLでも同様に使用可能です
ループと条件分岐
- GLSLは動的ループや条件分岐をサポートするため、HLSLの静的ループ展開を動的に書き換え可能です
注意点
- GLSLのバージョンによって機能差があるため、ターゲット環境に合わせてコードを調整
- OpenGLの座標系はDirectXと異なるため、座標変換や行列の順序に注意
これらのポイントを踏まえ、HLSLからGLSLへのシェーダー変換を行うことで、DirectX9のスキニング実装をOpenGL環境にスムーズに移植できます。
まとめ
本記事では、DirectX9を用いたC++でのスキニング実装に関する基礎から応用まで幅広く解説しました。
ボーン行列の計算や頂点シェーダーでの変換、パフォーマンス最適化、メモリ効率化、複数キャラクター描画、物理連携、表情アニメーションの統合、さらにはクロスプラットフォーム移植までカバーしています。
これらの知識を活用することで、リアルで効率的な3Dキャラクターアニメーションを実現し、幅広い環境での開発に対応可能となります。