【MMEの作り方】オブジェクトエフェクト編02/オブジェクトのビルボード化とパーティクル
▼今回の記事ページで使うサンプルファイル
今回の記事ぺージの解説で使うサンプルとなるfxファイルは以下のリンクからダウンロードできます。
ただし、ファイルをDL・使用した際にトラブル・不利益が生じたとしても筆者は責任を負わないものとします。自己責任で御利用ください。
▼Xファイルをパーティクルにしたい…その下ごしらえ
■板ポリは視点を動かすとペラいオブジェクトなのがバレる
Xファイルをfxファイルに紐づけるとエフェクトを割り当てた状態でMMDに呼び出せることを前回の記事で解説しました。
そのことを応用して、作例としてパーティクルエフェクトを作ってみたいと思います。
パーティクルエフェクトとは以下のgifアニメのように、空間上に粒子を発生させるエフェクトをいいます。

パーティクルエフェクトの一つ一つの粒子は、実は平たい板ポリで出来ています。
しかし、板ポリをただ空間上に設置したり移動させただけでは3Dビューを移動・回転させると厚みの無いペラいオブジェクトなのがバレてしまいます。
これでは見栄えが悪い。
それを防ぐためにオブジェクトのビルボード化という技法を使います。
■ビルボード化とは
オブジェクトのビルボード化とは、オブジェクトをどの視点から見てもオブジェクトが同じ面を向くようにする技法をいいます。

では、実際にXファイルの板ポリオブジェクトを実際にビルボード化する方法を解説していきます。
▼ビルボード化できるfxファイル
では早速サンプルファイル『2-2-1_Billboard.x』をMMDにインポートしてください。
オブジェクトを3Dビューの視点を動かして見てみると面の正面が常に画面の方を向いています。
では、次にXファイルのビルボード化を実現しているfxファイルを開いて下さい。
色々書いてありますが、大事なのは頂点シェーダーの部分です。
// -------------------------------------------------------------
// 頂点シェーダ(オブジェクトが常にカメラの方を向く)
// -------------------------------------------------------------
struct VS_OUTPUT {
float4 Pos : POSITION;
float2 Tex : TEXCOORD0;
};
VS_OUTPUT Basic_VS(float4 Pos : POSITION, float2 Tex : TEXCOORD0)
{
VS_OUTPUT Out;
float3 WorldUp = float3(0, 1, 0); // ワールド上方向
float3 local = Pos.xyz * BillboardScale;
float3 objPos = GetObjectWorldPos();
// カメラ方向(ワールド)
float3 look = - CameraPos - objPos;
float lookLen = length(look);
if (lookLen < 0.001) look = float3(0, 0, 1); else look /= lookLen;
// 右ベクトルを算出(WorldUp と look の外積)
float3 right = normalize(cross(WorldUp, look));
// look と right のクロスで正確な up を算出(直交基底)
float3 up = normalize(cross(look, right));
// ローカル頂点 -> ワールド
float3 worldPos = objPos + right * local.x + up * local.y + look * local.z;
Out.Pos = mul(mul(float4(worldPos, 1.0), View), Projection);
Out.Tex = Tex;
return Out;
}では、コードのそれぞれについてを見ていきましょう。
float3 GetObjectWorldPos()
{
return float3(WorldMatrix._41, WorldMatrix._42, WorldMatrix._43);
} オブジェクトのワールド座標を取得しています。
ワールド行列の右下(第4列)に位置ベクトルが格納されています。
float3 look = - CameraPos - objPos; オブジェクトの位置 objPos からカメラ位置 CameraPos への方向を計算。
この符号(- CameraPos - objPos)は、環境によって左右反転させる必要がある場合がありますが、ここではこのような記述になっています。
これにより「カメラを向く方向ベクトル」が求まります。
float3 right = normalize(cross(WorldUp, look));
float3 up = normalize(cross(look, right));
・WorldUp(ワールドの上方向)と look の外積で「右方向」を得る。
・look と right の外積で「正確な上方向」を得る。
この3つのベクトル (right, up, look) は直交座標系を構成し、これでカメラを向くローカル座標軸ができます。
float3 worldPos = objPos + right * local.x + up * local.y + look * local.z;ローカル空間の頂点 (local) をright, up, look 軸に沿って再配置することで、板がカメラを向くようになります。
Out.Pos = mul(mul(float4(worldPos, 1.0), View), Projection);ワールド空間で計算した頂点をビュー行列(カメラ空間)→射影行列(スクリーン空間)に変換します。
▼Y軸固定のビルボード化
上記のfxファイルは、オブジェクト全体をカメラに対して同じ面を向ける仕様でした。
しかし、場合によってはY軸(高さ方向)だけは固定した状態でクルクル回したいこともあるかもしれません。
なので上記コードを改修してY軸固定のビルボード化エフェクトもサンプルファイル『2-2-2_Billboard_yLock.x』として付属しておきます。

▼パーティクルエフェクトの中身
ビルボード化の実装により、パーティクルエフェクトを実装する準備が出来ました。
では実際にパーティクルエフェクトの中身を見ていきましょう。
サンプルファイル『2-2-3_Particle.x』をMMDに呼び出してみてください。
(※パーティクルの粒子が白い丸なので背景黒化して見やすくするのを推奨)
static float4 getRandom(float index)
{
float px = frac(sin(index * 0.8031));
float py = frac(sin(index * 0.8055));
float pz = frac(sin(index * 0.7077));
return float4(px, py, pz, 1);
}
index(粒子番号)を使って、sin()とfrac()の組み合わせで擬似乱数を生成。
GPUシェーダーでは rand() が使えないため、こういった「数式ノイズ」でランダム値を作ります。
float4 getParticlePos(float index, float time) {
float4 base_pos = getRandom(index);
float stime = time * DropSpeed;
base_pos.x += stime * DropVec.x + index;
base_pos.y -= stime * (0.5 + DropVec.y) + index;
base_pos.z += stime * DropVec.z + index;
} 各粒子は「インデックス(番号)」ごとに別のランダム基準位置を持ちます。
time が経過するたびに、DropVec方向に動き続けます。
つまり時間経過による落下・流れの再現しています。
float3 AreaCenterT;
AreaCenterT.x = -50 + AreaCenter.x;
AreaCenterT.y = 30 + AreaCenter.y;
AreaCenterT.z = -10 + AreaCenter.z;
float3 RotinvCenter = mul(AreaCenterT, RotMatrixInverse);
base_pos.xyz -= RotinvCenter;
float3 inner_pos = frac(base_pos.xyz) * 100;
base_pos.xyz = inner_pos + RotinvCenter; エリアの基準座標を決めて、粒子がその範囲内を循環する処理です。
frac() を使うことで位置を0〜1の範囲に繰り返し、 時間が経つとまた同じエリア内に戻る処理にしています。
つまり永続的に流れ続けるパーティクル表現になっています。
base_pos.x *= AreaSize.x;
base_pos.y *= AreaSize.y;
base_pos.z *= AreaSize.z;
return base_pos;「エリアの広さ」でスケーリングしてワールド上の最終位置を決定します。
【目次ぺージに戻る】
