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

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

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

今回でブログ作成は2回目ですが、今回もタイトル通りレイマーチングの解説です。

この記事は1回目のレイマーチング基礎の続きとなっています。
1つのオブジェクトを影を付けて描画するところから発展して、複数オブジェクト描画とCSG(Constructive Solid Geometry)を実装していきます。
CSGができるようになると複雑な形のオブジェクトを作れるようになるので、より一層おもしろく感じられると思います!

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

複数オブジェクトの描画

今までは単一のオブジェクトしか描画できていなかったので、複数描画してみましょう、後に説明するCSGにも必要になってくるので解説していきます。

コードは前回の記事の最後から引き継いだものとなります。

同じオブジェクトの描画

まずは球体を画面に2つ表示してみましょう変更箇所はここだけです。

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

    return min(d0, d1);
}

sphere_dは球の距離関数であり、距離関数はレイの先端とオブジェクトの最短距離を表すので、d0d1を比較し近いほうを返すことにより、複数のオブジェクトを描画することができます。
またレイの先端座標であるposvec3(0.5, 0.0, 0.0)を足すことによって、オブジェクトの位置をずらしています。
これはオブジェクト自体を動かすというより、レイをずらすことによって相対的に移動を実現しています。
主観ですがレイマーチングを含むレイトレースではよくオブジェクトの回転や移動をレイによって解決しています。

描画位置をずらす処理と2つの距離関数をsphere_d内にまとめている理由は、法線ベクトルを求める関数getNormalsphere_dを使用しているからです。

※2つの球を同時に移すためにカメラのz座標を1→100へ変更しています。

実行結果

立体視で使う画像みたいになりました。

異なるオブジェクトの描画

次は球と箱型を同時に描画してみたいと思います。
箱型を描画するには球の距離関数とは別に箱型の距離関数を使う必要があります。
関数はこちらのサイトのものを使用させていただきました。

箱型の距離関数の解説

下記は使用する箱型の距離関数です。
sphere_dbox_dに変更するだけで箱型が描画されます。

float box_d(vec3 pos, vec3 size)
{
    vec3 q = abs(pos) - size;
    return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}

この処理でなぜ箱型を描画できるか、ざっくりとですが解説したいと思います。

まず初めに上記の関数は3次元ですがわかりやすくするため、原点を中心とした2次元の正方形と任意の点との距離を計算するものとして考えていきます。
まずabs(pos)によって座標を絶対値に変換します、これは計算を第1象限だけに絞るためです。
次にxとy要素それぞれで考え、どちらかの要素で正方形の範囲内だった場合 (今回はyが範囲内と考える) 、距離はx - 正方形のサイズになります。
どちらの要素も範囲外だった場合は、頂点と点との距離になるのでlengthで計算できます。
これら二つの計算式を一つで表現できるのようにしたのがlength(max(q - size, 0.0))になります。

しかしながら上の計算式は点が箱型の中にいると0を返してしまいます。
ですのでmax(q.x, max(q.y, q.z))で点から一番近い平面を調べ、その平面と点の距離を内外判断をできるようにマイナスで返します。
minが使用されているのは点が箱型の外にいる際に、計算を行わないようにするためです。

箱型と球のオブジェクトを描画するために新しく関数distanceを追加したコードです。

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

// 箱型の距離関数
float box_d(vec3 pos, vec3 size)
{
    vec3 q = abs(pos) - size;
    return length(max(q, 0.0)) + min(max( q.x, max(q.y, q.z)), 0.0);
}

// 距離関数をまとめたもの
float distance(vec3 pos, float size)
{
    return min(box_d(pos + vec3(  0.5, 0.0, 0.0), vec3(size)),
            sphere_d(pos + vec3( -0.5, 0.0, 0.0), size));
}

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

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

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

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

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

    fragColor = vec4(col, 1.0);
}

実行結果

違う形のオブジェクトが同時に描画できました。
レイマーチの実装は結構独特なので色々な発見があります。

CSG(Constructive Solid Geometry)

簡単なオブジェクトを同時に描画する事はできましたが、もっと色々な形を描画したいと思いませんか?

そこで使用するのがCSGです。

CSGとは

2つのオブジェクトを集合論( 和集合共通部分差集合 )などを用いて描画する方法です。
一見難しそうに聞こえますが、やっていることはとても簡単ですので、 早速箱型と球で確認していきましょう。

ちなみに和集合はオブジェクト両方をそのまま描画するだけなのですでにやっていますね。

共通演算

float distance(vec3 pos, float size)
{
    float d0 = sphere_d(pos + vec3( 0.0, 0.0, 0.0), 0.4);
    float d1 =    box_d(pos + vec3( 0.0, 0.0, 0.0), vec3(size));
    
    return max(d0, d1);
}

minからmaxに変更したため、オブジェクトとレイが衝突したと判定するにはd1d0どちらかが0でもう片方が0以下でなければいけません、その結果どちらかのオブジェクトとレイが衝突しても続行され、重なっている部分のみが描画されたように見えます。

差集合

float distance(vec3 pos, float size)
{
    float d0 = sphere_d(pos + vec3( 0.0, 0.0, 0.0), 0.41);
    float d1 =    box_d(pos + vec3( 0.0, 0.0, 0.0), vec3(size));

    return max(-d0, d1);
}

共通集合からd0の符号を反転しました、これにより球の表面から外側ではd0が選ばれなくなり、球が描画されなくなります。
また球の内側にレイの先端がある際に箱型とレイが衝突した場合は、-d0が戻り値として使用されるので、箱型が球でえぐられたように描画されます。

いかがでしょうか?
コードを少しいじるだけで、とても簡単に形を変えることができました。
たくさんのオブジェクトをCSGで描画すると下のような変わった形も作れます。

おわりに

以上解説になりました 。
前回よりも簡単だったかなと思います、レイマーチは色々な要素入れていく毎に大幅に変わっていくので、楽しみながら調べられます。
距離関数は世の中にたくさん出回っているので色々試して、遊んでみてください。


【免責事項】

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