#概要
自作ゲームでキャラの攻撃モーションに合わせて軌跡を描画するシステムを作ったので紹介します。
DirectX11でBoxやSphereを描画できる人向けに書いています。
なのでレンダリングパイプラインや頂点バッファなどは一切説明しません。
#目次
-
剣を描画する仕組みを考えてみる
-
システム概要
-
データ構造体
-
描画クラス
-
シェーダー
-
実行結果
-
剣の軌跡を滑らかに補完する
-
CatMull-Romスプライン曲線
-
頂点を補完して分割する関数を追加する
-
実行結果
-
まとめ
#1.剣を描画する仕組みを考えてみる
今回実装した方法はとても単純です。
剣の位置を更新する時にバッファに剣の位置を保存します。
その情報から頂点バッファを作ってパイプラインに渡して処理をすることで軌跡が描画されます。
下の画像はそのイメージです。
#2.システム概要
今回作ったシステムでは軌跡のデータを保持する「データ構造体」とデータを元に軌跡を実際に描画する「描画クラス」に分けています。
##2-1.データ構造体
剣の先端と末端をひとまとめにします。
//剣の位置を保存する構造体
struct PosBuffer
{
XMFLOAT3 head; //剣の先端の位置
XMFLOAT3 tail; //剣の末端の位置
};
//頂点バッファ
struct SwordTrailVertex
{
XMFLOAT3 pos = { 0, 0, 0 };
XMFLOAT2 uv = { 0, 0 };
};
実際はこのデータ構造体を配列として利用します。
以下が剣の軌跡を管理するクラスです。
class SwordTrail
{
private:
std::vector<PosBuffer> posArray; //剣の位置を保存するバッファ
std::vector<SwordTrailVertex> vertex; //頂点バッファ
PosBuffer tempPos; //現在フレームでの剣の位置
public:
SwordTrail(int bufferSize); //コンストラクタでposArrayのサイズを決める
void Update(); //履歴を更新して、頂点バッファを更新する
void SetPos(XMFLOAT3& head, XMFLOAT3& tail); //現在フレームの剣の位置を保存する
//省略...
};
void SwordTrail::Update()
{
//データを更新
for(size_t i = posArray.size() - 1; i > 0; --i)
{
posArray[i] = posArray[i - 1];
}
posArray.front() = tempPos;
tempPos = PosBuffer();
//曲線を作る
std::vector<PosBuffer> usedPosArray = GetUsedPosArray();
if ( usedPosArray.empty() )return;
CreateCurveVertex(usedPosArray);
//頂点データを更新する
float amount = 1.0f / (usedPosArray.size() - 1);
float v = 0;
vertex.clear();
vertex.resize(usedPosArray.size() * 2);
for ( size_t i = 0, j = 0; i < vertex.size() && j < usedPosArray.size(); i += 2, ++j)
{
vertex[i].pos = usedPosArray[j].headPos;
vertex[i].uv = XMFLOAT2(1.0f, v);
vertex[i+1].pos = usedPosArray[j].tailPos;
vertex[i+1].uv = XMFLOAT2(0.0f, v);
v += amount;
}
}
void SwordTrail::SetPos(XMFLOAT3& head, XMFLOAT3& tail)
{
tempPos.headPos = headPos;
tempPos.tailPos = tailPos;
tempPos.isUsed = true;
}
##2-2.描画クラス
class SwordTrailRenderer
{
public:
SwordTrailRenderer(); //コンストラクタ(Initalizeを呼び出すだけ)
void Initialize(); //初期化
void Render(uint32_t vertexNum, ID3D11Buffer* vertexBuffer, ID3D11ShaderResourceView* texture); //描画
private:
bool isInitialized = false;
ID3D11VertexShader* vertexShader;
ID3D11PixelShader* pixelShader;
ID3D11BlendState* blendState;
ID3D11RasterizerState* rasterizer;
ID3D11DepthStencilState* depthStencil;
ID3D11InputLayout* layout;
};
描画クラスはテクスチャを持っていません。
1つの描画クラスで複数の剣の軌跡を描画するために、テクスチャはSwordTrailクラスに持たせています。
頂点バッファとテクスチャ、描画する頂点の数を引数に軌跡を描画します。
##2-3.シェーダー
struct VS_IN
{
float3 pos : POSITION;
float2 uv : TEXCOORD0;
};
struct PS_IN
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
Texture2D swordTex : register(t0);
SamplerState swordSampler : register(s0);
PS_IN main(VS_IN input)
{
PS_IN result = (PS_IN) 0;
result.pos = mul(g_xCamera_VP, float4(pos, 1));
result.uv = uv;
return result;
}
float4 main (PS_IN input) : SV_TARGET0
{
float4 color = SwordTex.SampleLevel(Sampler, input.uv, 0);
return color;
}
シェーダーでは特に特別なことはしません。
頂点をカメラの行列で計算してpos、uvをピクセルシェーダに渡します。
##2-4.実行結果
この段階では剣の軌跡がカクカクになってしまいます。
ですので補完を掛けて滑らかにします。
#3.剣の軌跡を滑らかに補完する
軌跡を滑らかにするには、各頂点の間を結ぶカーブを求めて、頂点を増やす(分割する)必要があります。
##3-1.CatMull-Romスプライン曲線
CatMull-Romスプライン曲線は4つの制御点を使います。
そしてできた曲線は4つの制御点を全て通ります。(調べたところそういう性質があるみたいです。)
実際にプログラムを組む際はDirectXMathのXMCatMullRom関数を使うと便利です。
//要約するとPos1からPos2へt%すすんだ位置を返す。
XMVectorCatmullRom(XMVECTOR pos0, XMVECTOR pos1, XMVECTOR pos2, XMVECTOR pos3,float t);
この関数のイメージはこんな感じです。
pos0とpos3を使ってpos1とpos2を結ぶカーブを作成します。
実際は、軌跡の端は頂点が足りないのでごまかしてます。
##3-2.頂点を補完して分割する関数を追加する
今回の実装ではSwordTrailクラスにCreateCurveVertexという関数を追加し、頂点を任意の数で分割しています。
void SwordTrail::CreateCurveVertex(std::vector<PosBuffer>& usedPosArray)
{
if ( usedPosArray.size() < 3 || split < 1) { return; }
std::vector<PosBuffer> newPosArray;
newPosArray.reserve(usedPosArray.size() + ( usedPosArray.size() - 1 ) * split);
const float amount = 1.0f / ( split + 1 );
PosBuffer newPos;
newPosArray.push_back(usedPosArray.front());
for ( size_t i = 0; i < usedPosArray.size() - 1; ++i )
{
float ratio = amount;
// CatMulに使う4つの点を作る(p0, p3がない時の処理も書く)
XMVECTOR p0Head = i == 0 ? (XMLoadFloat3(&usedPosArray[1].headPos) + XMLoadFloat3(&usedPosArray[2].headPos)) * 0.5f : XMLoadFloat3(&usedPosArray[i-1].headPos);
XMVECTOR p1Head = XMLoadFloat3(&usedPosArray[i].headPos);
XMVECTOR p2Head = XMLoadFloat3(&usedPosArray[i+1].headPos);
XMVECTOR p3Head = i == usedPosArray.size() - 2 ? ( p0Head + p2Head ) * 0.5f : XMLoadFloat3(&usedPosArray[i+2].headPos);
XMVECTOR p0Tail = i == 0 ? ( XMLoadFloat3(&usedPosArray[1].tailPos) + XMLoadFloat3(&usedPosArray[2].tailPos) ) * 0.5f : XMLoadFloat3(&usedPosArray[i-1].tailPos);
XMVECTOR p1Tail = XMLoadFloat3(&usedPosArray[i].tailPos);
XMVECTOR p2Tail = XMLoadFloat3(&usedPosArray[i + 1].tailPos);
XMVECTOR p3Tail = i == usedPosArray.size() - 2 ? ( p0Tail + p2Tail ) * 0.5f : XMLoadFloat3(&usedPosArray[i + 2].tailPos);
for ( size_t j = 0; j < static_cast<size_t>(split - 1); ++j)
{
newPos = PosBuffer();
newPos.isUsed = true;
XMStoreFloat3(&newPos.headPos, XMVectorCatmullRom(p0Head, p1Head, p2Head, p3Head, ratio));
XMStoreFloat3(&newPos.tailPos, XMVectorCatmullRom(p0Tail, p1Tail, p2Tail, p3Tail, ratio));
newPosArray.push_back(newPos);
ratio += amount;
}
newPosArray.push_back(usedPosArray[i+1]);
}
usedPosArray = newPosArray;
}
##3-3.実行結果
2-4の実行結果とくらべると面が滑らかになりました。
【左:補完なし 右:補完あり】
#4.まとめ
今回は、剣の軌跡を描画する方法を紹介しました。
このシステムがあればすぐに描画できるかといえばそうではありません。
実際使うときは、プログラマー側で剣のボーンの位置を取得できるようにしないといけません。
そのあたりは、また時間がある時に記事にするかもしれません。