1. サイトトップ
  2. ブログ
  3. 【Shadertoy】レイマーチング基礎

【Shadertoy】レイマーチング基礎

こんにちは!情熱開発部の辻野です!

4月になりあっという間に入社二年目となりましたが、初めて技術ブログを書かせていただくということで、緊張しております。

さて今回のテーマはレイマーチングということで少し難しい印象がありますが、できるだけイメージがしやすいように解説をしていきたいと思います。

※今回の記事ではShaderToyを用いたプログラミングとなります、他のプラットフォームによって一部表記が変わるためご注意ください。

レイマーチングとは

まず初めにレイマーチングはレイトレーシングの一種です。
レイトレーシングは視点に入ってくる光(レイ)の経路を追跡(トレース)する手法で、レイマーチングはトレースの際に光(レイ)を行進(マーチ)させるため、レイマーチングと呼ばれます。

現実世界では光源から放たれた光を物体が反射し、その光が目に入ることによって物体の色を認識することができるというわけですが、レイトレーシングは光源から全ての光を追跡するのではなく、効率的に描画するために「視点に届いた光のみを追跡」しています。
つまりレイトレーシングでは「視点から光源へと光を逆向きに追跡している」ということになります。

レイマーチングの仕方

では実際に確認していきましょう。

  1. カメラの位置を決定
  2. レイの方向を決定
  3. レイの先端から一番近いオブジェクトへの距離を求める
  4. (3)で求めた距離分だけレイを進める
  5. レイがオブジェクトに衝突していなければ(3)に戻る。
    一定回数繰り返す、または衝突した場合は終了

レイマーチングの動作イメージ

レイが行進していますね。
以下はレイマーチの実装コードです。

// 球の距離関数
// オブジェクトの中心を原点とした時の引数の位置と球の距離を返す
float sphere_d(vec3 pos, float size)
{
    return length(pos) - size;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // 描画位置がpxで返ってくるので-1.0 ~ +1.0に正規化
    vec2 pos = (fragCoord.xy * 2.0 - iResolution.xy) / min(iResolution.x, iResolution.y);
    vec3 col = vec3(0.0);
  
  // カメラの位置
    vec3 cPos = vec3(0.0, 0.0, -1.0);
  
  // レイの方向(カメラから描画位置へのベクトル)を決定
    vec3 rayDir = normalize(vec3(pos, 0.0) - cPos);
    vec3 cur = cPos;
    
    // 描画する球のサイズ
    float size = 0.3;
  
    // 今回はレイの進める回数が最大16回
    for (int i = 0; i < 16; i++)
    {
        // レイの先端と球の距離を計測
        float d = sphere_d(cur, size);

        // 距離が限りなく0に近い=レイと球が衝突している
        if (d < 0.0001)
        {
            col = vec3(1.0);
            break;
        }
        // レイを進める
        cur += rayDir * d;
    }

    fragColor = vec4(col, 1.0);
}

実行結果

白い円が表示されました、仕組みが分かっていても不思議ですね。

距離関数について

今回の実装で球との衝突判定をする際に用いた関数

float sphere_d(vec3 pos, float size)
{
    return length(pos) - size;
}

を距離関数といいます。
距離関数はオブジェクトの中心を原点とした際にオブジェクトと座標の距離を返す関数で、今回は球の距離関数を用いたので球が描画されましたが、他の距離関数を使用することで様々な形状を描画することができます。
色んな距離関数がまとめられたサイト

ライティング

現状だと円なのか球なのかよくわからないので法線を用いてライティングを行いたいと思います。

vec3 getNormal(vec3 pos, float size)
{
    float delta = 0.0001;
    return normalize(vec3(
            sphere_d(pos, size) - sphere_d(vec3(pos.x - delta, pos.y, pos.z), size),
            sphere_d(pos, size) - sphere_d(vec3(pos.x, pos.y - delta, pos.z), size),
            sphere_d(pos, size) - sphere_d(vec3(pos.x, pos.y, pos.z - delta), size)
        ));
}

これが法線を取得する関数になります、ぱっと見何をやっているかわかりにくいと思いますが、XYZ各成分で微分(偏微分)を行い勾配を計算しています。
詳しく解説するとかなり長くなってしまうため、法線の求め方の解説が乗っている高校数学の美しい物語さんの記事を紹介させていただく形で省かせていただきます。

ライティングを実装したコードです。

// 球の距離関数
// オブジェクトの中心を原点とした時の引数の位置と球の距離を返す
float sphere_d(vec3 pos, float size)
{
    return length(pos) - size;
}

// レイがぶつかった位置を偏微分をして球の法線ベクトルを計算
vec3 getNormal(vec3 pos, float size)
{
    float delta = 0.0001;
    return normalize(vec3(
            sphere_d(pos, size) - sphere_d(vec3(pos.x - delta, pos.y, pos.z), size),
            sphere_d(pos, size) - sphere_d(vec3(pos.x, pos.y - delta, pos.z), size),
            sphere_d(pos, size) - sphere_d(vec3(pos.x, pos.y, pos.z - delta), size)
        ));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // 描画位置がpxで返ってくるので-1.0 ~ +1.0に正規化
    vec2 pos = (fragCoord.xy * 2.0 - iResolution.xy) / min(iResolution.x, iResolution.y);
    vec3 col = vec3(0.03);
    
    // ライトの方向と色を決定
    vec3 lightDir = normalize(vec3(0.4, 1.0, 0.8));
    vec3 lightCol = vec3(0.9);
    
    // カメラの位置
    vec3 cPos = vec3(0.0,  0.0,  1.0);

    // レイの方向(カメラから描画位置へのベクトル)を決定
    vec3 rayDir = normalize(vec3(pos, 0.0) - cPos);
    vec3 cur = cPos;
    
    // 描画する球のサイズ
    float size = 0.3;

    // 今回はレイの進める回数が最大16回
    for (int i = 0; i < 16; i++)
    {
        // レイの先端と球の距離を計測
        float d = sphere_d(cur, size);

        // 距離が限りなく0に近い=レイと球が衝突している
        if (d < 0.0001)
        {
            // レイが衝突した位置における法線ベクトルを取得
            vec3 normal = getNormal(cur, size);
            
            // 法線ベクトルとライトのベクトルで内積をとる(ランバート反射)
            float diff = dot(normal, lightDir);

            // 内積結果とライトの色をかけて描画色を決定
            col = vec3(diff) * lightCol;
            break;
        }
        // レイを進める
        cur += rayDir * d;
    }

    fragColor = vec4(col, 1.0);
}

実行結果

いい感じに影がつきました。

おわりに

以上解説になりました。
法線を求める関数など一部数学的知識が必要とされるところもありますが、基本的な仕組み自体は現実のものを参考にしているので、割となじみやすく感じました、モデルがなくても描画ができるのはとても面白いですね。
またShardertoyはお手軽に実行確認ができるので試してみてください。


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。