【MMEの作り方】シェーダーエフェクト編05/レイマーチングという手法
▼今回の記事ページで使うサンプルファイル
今回の記事ぺージの解説で使うサンプルとなるfxファイルは以下のリンクからダウンロードできます。
ただし、ファイルをDL・使用した際にトラブル・不利益が生じたとしても筆者は責任を負わないものとします。自己責任で御利用ください。
▼レイマーチングの基礎…なのだけど
シェーダーエフェクト編の最後はレイマーチングについて解説して締めとします。
といっても、レイマーチングという手法自体は処理が重く、多分、MMDで使う分には適しません。
なので「世の中にはレイマーチングなる手法というものがあるのだなあ」くらいのオマケ程度に受け取っていただければ幸いです。
レイマーチングというのは、レイ(光線)を少しずつ進めながら、オブジェクトとの距離を調べて描画する方法です。
■レイマーチングのメリットとデメリット
レイマーチングのメリットとデメリットの例は以下の通り。
◎メリット
・ポリゴンモデルがなくても、純粋な数式だけで3D形状を作れる
・ブーリアン演算(合成・差・交差)が簡単
・フラクタルやボリューム、ソフトシャドウなども表現しやすい
◎デメリット
・計算コストが高い(多くのステップを踏む)
・複雑なシーンではパフォーマンスが低下
・アニメーションや衝突判定などが難しい
■レイマーチングのステップ
①カメラから光線(レイ)を飛ばす
各ピクセルに対して、カメラ(視点)からその方向にレイを一本飛ばす。
②シーンとの距離を測る(距離関数)
そのレイが進む先に「どれくらい進めばオブジェクトにぶつかるか」を表す距離関数を使って距離を算出。
③少しずつ進める(マーチング)
得られた距離だけレイを進め、再びその地点で距離を計測。
オブジェクト表面に近づくまでこれを繰り返す。
④十分近づいたらヒット(衝突)とみなす
距離が一定以下(例:0.001以下)になったら「表面に当たった」と判断。
⑤ヒットした点で色や法線を計算する
そこでライティングやシェーディングを行い、ピクセルの色を決定。
……と説明してもイメージが湧きにくいので実際にレイマーチングを使ったシェーダーエフェクトを見ていきましょう。
▼距離関数を使って球体を表示する
では、サンプルファイルから『1-5-0_RM_Cube.pmx』をMMD内に設置して、『1-5-1_RM_Sphere_Base.fx』を割り当ててみてください。
青い立方体の中に白い丸(球)が表示されています。
3Dビューを動かすと球が立方体の中に留まったまま表示される不思議なオブジェクトになっています。
これが単純な形のレイマーチングの例です。

では、これがどんなコードによって出来ているか見ていきましょう。
まず頂点シェーダーに関して。
ここで、とりあえずレイマーチングでは立方体のメッシュオブジェクトの頂点位置を確定させるだけでなく、ワールド座標とカメラ位置が必要なのが分かります。
struct VS_OUTPUT
{
float4 Pos : POSITION;
float3 WorldPos : TEXCOORD0;
float3 Eye : TEXCOORD1;
};
VS_OUTPUT Basic_VS(float4 Pos : POSITION)
{
VS_OUTPUT Out;
// スクリーン上での頂点の位置
Out.Pos = mul(Pos, WorldViewProj);
// ワールド座標
float4 worldPos = mul(Pos, World);
Out.WorldPos = worldPos.xyz;
// カメラ位置
Out.Eye = CameraPos;
return Out;
} 続いて距離関数とピクセルシェーダーの部分に関して。
この部分で実際のレイマーチングの実装を行っています。
// -------------------------------------------------------------
// 距離関数
// -------------------------------------------------------------
float sphere(float3 p) // 球の距離関数
{
return length(p - CenterPos) - Size;
}
// -------------------------------------------------------------
// ピクセルシェーダ関連
// -------------------------------------------------------------
float4 Basic_PS(float3 WorldPos : TEXCOORD, float3 Eye : TEXCOORD1) : COLOR0
{
float3 ro = Eye; //レイのスタート位置を視点の座標にする
float3 rd = normalize(WorldPos - ro); //メッシュのワールド座標からスタート位置を引いてレイの方向を定義
float d = 0;
float t = 0;
float3 p = float3(0, 0, 0);
for (int i = 0; i < 16; ++i) //レイマーチングのループ実行
{
p = ro + rd * t;
d = sphere(p);
t += d;
}
float4 ResultColor = float4(0, 0, 0, 1);
if (d > 0.01)
{
ResultColor = OutsideColor; //レイが衝突していないならパラメータ『OutsideColor』で描画
}
else
{
ResultColor = InsideColor; //レイが衝突しているならパラメータ『InsideColor』で描画
}
return ResultColor;
} ピクセルシェーダーの中身に色々と書いてあります。
具体的な内容についてはコード内の処理についてはコメントアウトに記載してある通りです。
置いておいて、まず注目してほしいのはfor文(繰り返し文)とif文(条件分岐)が使われている点です。
シェーダープログラミングにおいてif文は処理が重くなる原因となり、for文も複数回計算を繰り返す処理になります。
なのでレイマーチングは総じて処理が重くなる宿命を背負っています。
これが冒頭で書いた『レイマーチングという手法自体は処理が重く、多分、MMDで使う分には適しません』という文言の正体です。
▼レイマーチングの球体に影を付ける
上記の例で球体……というか、のっぺりした単色の丸を表示することは出来ました。
しかしこれだけではシンプルすぎるので、次は球体に影を付ける処理を実装します。
サンプルファイル『1-5-2_RM_Sphere_Shade.fx』をご覧ください。
実際に立方体に割り当てると以下のようになります。

影をつけるのに要となるのはコードの以下の部分です。
// -------------------------------------------------------------
// 関数:レイマーチング用の法線算出
// -------------------------------------------------------------
float3 getNormal(float3 p) {
float d = 0.001;
return normalize(float3(
sphere(p + float3(d, 0, 0)) - sphere(p + float3(-d, 0, 0)),
sphere(p + float3(0, d, 0)) - sphere(p + float3(0, -d, 0)),
sphere(p + float3(0, 0, d)) - sphere(p + float3(0, 0, -d))
));
} レイマーチングで表現されている球はメッシュオブジェクトではありません。
そのため、頂点シェーダーでは法線を追加できず、別途、法線に相当するものを設定する必要があります。
それを実現しているのがコードのこの部分です。
▼距離関数を使って立方体を表示する
では次に、レイマーチングで表示している形状を球から立方体に変更してみましょう。
サンプルファイル『1-5-3_RM_Cube.fx』をご覧ください。

前述の通りレイマーチングでは、距離関数がレイの衝突判定の要となっています。
ということは、距離関数の中身を書き換えることで表示される形状を変更できます。
ここでは、以下のコードのように立方体を表現できる距離関数に書き換えています。
float LengFanc(float3 p) // 立方体の距離関数
{
return length(max(abs(p - CenterPos) - float3(Size, Size, Size), 0.0));
}▼ハートを作る距離関数
距離関数さえ作れれば、レイマーチングでは多種多様な形状を表現できます。
この解説の作例では最後にレイマーチングでハート型を作ってみました。
サンプルファイル『1-5-4_RM_Heart.fx』を御参照ください。

この形状を作るための距離関数は以下の通り。
……形状が凝ったものになってくると、距離関数もそれなりに凝ったものになってくるのがレイマーチングなのです。
float LengFanc(float3 p) // ハートの距離関数
{
float Beat = 11 + sin(Time * 2);
return sqrt(length(p)*length(p) + pow(p.x * p.x + 0.1125 * p.z * p.z, 0.35)* -2 * p.y) - Beat;
}【目次ぺージに戻る】
