Ray-MMD にレイマーチングを使ったエフェクトを組み込む方法を解説します。
レイマーチングそのものについてはこの記事では説明しません。
ゴール
この記事のゴールは次の画像のように Ray-MMD の普通のシーンの中にレイマーチングでオブジェクトを書き込むことです。青緑色の球がレイマーチングで描かれたオブジェクトです。
前提
レイマーチングを動かすには自作のピクセルシェーダを Ray-MMD に組み込んで動かす必要があります。そのやり方は以下の記事で説明しているのでそれを参照してください。
本記事では「Ray-MMD に自作ポストエフェクトを差し込んでみる」の PostEffectExamplePS
を書き換えてレイマーチングを行います。
ソースコード
ピクセルシェーダのコードです。大体は普通のレイマーチングなのでレイマーチングを知っている人ならば問題なく読めるかと思いますが、一部 MMD 上で動かすためのコードがあるのでその部分を解説していこうと思います。
static float SphereRadius = 30;
inline float SDF(float3 p) {
return length(p) - SphereRadius;
}
inline float3 NormalVectorAt(float3 p) {
static const float EPS = 0.01;
float v0 = SDF(p);
float vx = SDF(p - float3(EPS, 0, 0));
float vy = SDF(p - float3(0, EPS, 0));
float vz = SDF(p - float3(0, 0, EPS));
return normalize(float3(v0 - vx, v0 - vy, v0 - vz));
}
// 点pの深度を求める。
inline float LinearDepthAt(float3 p, float3 cameraPos, float3 cameraDir) {
return dot(p - cameraPos, cameraDir);
}
// cameraPos から rayDir の方向にレイを飛ばし、オブジェクトの表面に衝突する点を求める。
// 衝突する場合、cameraPos から衝突点までの距離を返す。
// 衝突しない場合は -1 を返す。
float CastRay(float3 rayDir, float3 cameraPos, float3 cameraDir, float maxDepth) {
static const int N_STEPS = 100;
static const float EPS = 0.01;
float d = 0;
[fastopt]
for (int i = 0; i < N_STEPS; i++) {
float3 p = cameraPos + d * rayDir;
if (LinearDepthAt(p, cameraPos, cameraDir) > maxDepth) {
return -1;
}
float dist = SDF(p);
if (dist < EPS) {
return d + dist;
}
d += dist;
}
return -1;
}
// 物体の表面の色を計算する。
float3 CalculateSurfaceColor(float3 surfacePos, float3 cameraPos) {
static const float3 BASE_COLOR = float3(0.0, 0.43, 0.33);
static const float3 AMBIENT_COLOR = BASE_COLOR * 0.1;
static const float SHININESS = 30;
float3 normal = NormalVectorAt(surfacePos);
// 拡散光
float dotLN = saturate(dot(-SunDirection, normal));
float3 diffuse = dotLN * BASE_COLOR / PI;
// 鏡面反射光
float3 viewDir = normalize(cameraPos - surfacePos);
float3 reflectDir = -reflect(-SunDirection, normal);
float dotRV = saturate(dot(reflectDir, viewDir));
float3 specular = pow(dotRV, SHININESS);
return (AMBIENT_COLOR + diffuse + specular) * SunColor;
}
// World空間におけるカメラとレイの位置や向きを求める。
// レイの向きは現在描画中のピクセルの方向を指していて、
// カメラの向きは現在描画中のピクセルとは関係なく画面の中央を指している。
void SetupCameraAndRay(float2 coord, out float3 oCameraPos, out float3 oCameraDir, out float3 oRayDir) {
float2 p = (coord.xy - 0.5) * 2.0;
oCameraPos = CameraPosition;
oCameraDir = normalize(matView._13_23_33 / matProject._33);
oRayDir = normalize(
matView._13_23_33 / matProject._33
+ matView._11_21_31 * p.x / matProject._11
- matView._12_22_32 * p.y / matProject._22
);
}
float4 PostEffectExamplePS(in float4 coord : TEXCOORD0) : COLOR
{
float4 MRT0 = tex2Dlod(Gbuffer5Map, float4(coord.xy, 0, 0));
float4 MRT1 = tex2Dlod(Gbuffer6Map, float4(coord.xy, 0, 0));
float4 MRT2 = tex2Dlod(Gbuffer7Map, float4(coord.xy, 0, 0));
float4 MRT3 = tex2Dlod(Gbuffer8Map, float4(coord.xy, 0, 0));
MaterialParam material;
DecodeGbuffer(MRT0, MRT1, MRT2, MRT3, material);
float3 cameraPos, cameraDir, rayDir;
SetupCameraAndRay(coord.xy, cameraPos, cameraDir, rayDir);
float d = CastRay(rayDir, cameraPos, cameraDir, material.linearDepth);
clip(d); // 物体に衝突しなかった場合、このピクセルは捨てる
float3 color = CalculateSurfaceColor(cameraPos + d * rayDir, cameraPos);
return float4(color, 1);
}
カメラとレイのセットアップ
レイマーチングを行うためにはレイを飛ばす方向を計算しないといけません。これをやっているのが SetupCameraAndRay
です。
oRayDir
を計算する式は サンドマン さんに教えてもらいました。私はこの式でレイの方向が正しく求まる理由を理解できていないので解説できません。誰か理由がわかった人は私に優しく教えて下さい(><)
[fastopt]
レイマーチングの for 文に [fastopt]
をつけておくとコンパイル速度がめちゃくちゃ速くなります。というか [fastopt]
をつけないとコンパイルが死ぬほど遅いです。ただし、[fastopt]
をつけるとループが unroll されなくなってしまうのでループ回数が多いものにだけつけるようにしましょう。
[fastopt]
for (int i = 0; i < N_STEPS; i++) {
...
}
深度
オブジェクトが別のオブジェクトの背後に隠れるようにするためには、深度を正しく計算しなければなりません。深度の計算をしているのは LinearDepthAt
です。
// 点pの深度を求める。
inline float LinearDepthAt(float3 p, float3 cameraPos, float3 cameraDir) {
return dot(p - cameraPos, cameraDir);
}
点pの深度は、p とカメラの距離 ではない ということに注意してください。私はこれに気付かず丸1日ぐらいハマりました。
点pの深度は、「カメラの座標を通り、カメラの正面方向を法線とする平面」から点pまでの最短距離です。
clip
clip
関数は HLSL の組み込み関数です。clip
は、与えられた値が負の数だったときに現在のピクセルを破棄します。例えば clip(-1)
を実行するとそのピクセルに関してはそこでピクセルシェーダの実行が中断され、そのピクセルへの描画が行われません。
今回の場合はオブジェクトとレイが衝突しなかったときにピクセルシェーダを終了する目的で使用しています。
おわりに
レイマーチングでよい MMD ライフを!