めーぷるのおもちゃばこ

- アイドルになりたいエンジニア女子の制作日記 -

【GLSL】【シェーダー】文系で数学なんもわからんエンジニアだって、シェーダー完全に理解したい!!〜レイマーチング入門編①〜

はじめに

最近GLSLにハマっているのですが、今回はレイマーチングを完全に理解したくてモクモクしたので、最初の第一歩、やり方を備忘録としてまとめようと思います🐼
こんな感じでやってみました↓↓ これを作りながら完全に理解だぜ🐼

f:id:maplesyrup-cs6:20190716162548g:plain
レイマーチング第一歩


※今回の記事ではShaderToyでのプログラムになります。glsl fanやveda、 GLSL editor などプラットフォームによってresolutionなどの表記が変わるので注意してください。

レイマーチングとは

とは

コードを書いてお絵かきをするとき、普通にかくと平面の描画、つまり二次元の描画になります。
でも、人類というのは貪欲で、未知の挑戦が好きなわけで、二次元の絵ばっか描いてたら今度は三次元の絵もコードで書きたい!ってなってくるんですよ。知らんけど。
そんな時に編み出した方法?がレイトレーシングと言われるもので、光(レイ)の経路を追跡(トレース)するという手法です。
今回のレイマーチングはそのレイトレーシングと呼ばれるものの一種で、レイ(光)をマーチ(行進)させ、レイが何かにぶつかったらそれを描画する、というものです。

レイマーチングのやり方

以下のGIFをご覧ください。
シェーダーでのレイマーチングは、スフィアトレーシングと言われる手法を使っています。
ブワッと円を広げて、ぶつかったらレイ(青色)を進め、また円を広げてぶつかったらレイを進め....を繰り返しています。
そしてぶつかったオブジェクトまでの距離が限りなく近くなったら、ぶつかったということにしてそこに色を塗ります。
スクリーンの全ピクセル分の方向に飛ばしたたくさんのレイ一つ一つがこの作業を繰り返すことで、描画が成されます。

f:id:maplesyrup-cs6:20190704175442g:plain
レイマーチング



実際にやってみよう〜レイマーチング第一歩〜

レイマーチングのやり方を手順的に説明していきます🐼

①最初の設定をするんだぜ🐼

最初に、3つ決めることがあります。
①レイを発射するカメラの位置
②スクリーン平面をどれだけカメラに近づけるか
③飛ばすレイの方向
です。

f:id:maplesyrup-cs6:20190704181847p:plain
カメラの位置とスクリーン

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // 正規化
    vec2 uv = (fragCoord*2.0 -iResolution.xy) / min(iResolution.x, iResolution.y);
    
    vec3 cameraPos = vec3(0., 0., -10. );  //カメラの位置
    float screenZ = 2.5;    //スクリーン平面をどれだけカメラに近づけるか
    vec3 rayDirection = normalize(vec3(uv, screenZ));  //rayの方向 
} 


まず、正規化 と書いているところ、これに関しては別の記事に書こうかと思うのでとりあえずここはとばします!
とりあえずはおまじないというか決まりごとというかそんな感じのものだと思ってください。
これについて書いた記事を書いたらここに貼り付けておきます。

カメラの位置
vec3 cameraPos = vec3(0., 0, -10. );  //カメラの位置

まず、カメラの位置です。どこにカメラを構えるかそのポジションを設定します。まんまUnityを考えていただくとわかりやすいです。
ここでは、スクリーンの位置からZ軸が-10の位置にカメラを構えました。


スクリーン平面をどれだけカメラに近づけるか
float screenZ = 2.5;    //スクリーン平面をどれだけカメラに近づけるか

Rayと聞くと、一直線に一本飛ばすイメージがありますが、実際にはスクリーンの全ピクセルの方向にとばし、一本一本上記のスフィアトレーシングを行なっています。
スクリーンの四隅のピクセルに向かって飛ばしたレイを可視化すると、カメラからスクリーンが近い方が角度が広がるので広角になってオブジェクトが小さく見え、遠いと角度が狭くなって望遠レンズのような感じになってオブジェクトが大きく見えます。

f:id:maplesyrup-cs6:20190708185139p:plain
スクリーンの位置


真上から見るとわかりやすいです。スクリーンの両はしのピクセルに向かって飛ばしたレイの角度が違います。

f:id:maplesyrup-cs6:20190708184649p:plain
上からの図


Rayの方向
vec3 rayDirection = normalize(vec3(uv, screenZ));  //rayの方向 

Rayを飛ばす方向を定めています。vec3(uv, screenZ); は、X軸とY軸はuvのピクセルの方向にとばし、Z軸はScreenの方向に飛ばす、ということです。
normalize()値を正規化してくれる関数で、本来であればRayがものすごい長さになって大きな値になるのを、normalizeすることで値を1に変換してくれます。
そうすることで計算がしやすくなります。


②レイを進めよう🐼

距離関数について

レイを進めてオブジェクトを描画するには、距離関数というものが必要になります。
距離関数は、スフィアトレーシングをするための道具だと考えるとわかりやすいです。
レイの進め方は、「その時点のレイの位置」から「一番近くにあるオブジェクトの距離を計算(=距離関数の戻り値)」して、それが限りなくゼロに近づいたらオブジェクトにぶつかったことにして色を塗る(=描画する)感じです。
距離関数は作りたいオブジェクトの形によって変わってきます。
以下のサイトは、ShaderToyを作った人が、距離関数を公開?している場所らしいです。ここからコピペしてきましょう!
www.iquilezles.org

今回は一番簡単なスフィアの距離関数を使用します。

float sdSphere( vec3 p, float s )
{
  return length(p)-s;
}


これが↑スフィアの距離関数です。引数のpはレイのポジション、sはスフィアのサイズです。
これでスフィアが出来上がるのが不思議ですね。
レイを進めながらこの距離関数に値を渡してあげることで、オブジェクトの描画ができます。

レイの進め方

まずはこういう絵を作ります。平面的な丸に見えますが、レイマーチングができています。

f:id:maplesyrup-cs6:20190703131530p:plain
レイマーチング第一歩
レイはfor文でちょっとずつ進めていきます。
レイを進める部分を追加して、全体的なコードは以下の感じです。一つづつ説明していきます🐼

    
float distanceFunction(vec3 pos, float size){    //距離関数
   
    return length(pos)-size;
}


void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = (fragCoord*2.0 - iResolution.xy) / min(iResolution.x, iResolution.y);

    vec3 CameraPos = vec3(0, 0, -10);
    float screenZ = 2.5;
    vec3 RayDir = normalize(vec3(uv, screenZ));
    
    float depth = 0.0;
    float dist = 0.0;
    vec3 rayPos = vec3(0.0);
    vec3 col = vec3(0.0);
    
    
    for(int i = 0; i < 99; i++){
        
        rayPos = CameraPos + (RayDir * depth);
        dist = distanceFunction(rayPos, 1.0);
        
        if(dist < 0.0001) break;
      	  
        depth += dist;
        
    }

    if(dist < 0.0001){
       
        col = vec3(1.);

    }
    
    fragColor = vec4(col,1.0);
}
    

ーーー

解説

レイを進めるために必要な変数
    float depth = 0.0;
    float dist = 0.0;
    vec3 rayPos = vec3(0.0);
    vec3 col = vec3(0.0);

レイを進める時に使用する変数を4つ用意します。
depthは「レイの進んだ距離」を入れるfloatの変数distは「距離関数から返ってきた値」を入れるfloatの変数rayPosは「現在のレイの位置」を入れるvec3の変数colは描画時の色を決めるvec3の変数です。
これを作ってからレイを進めます。
以下、レイを進めるコードです。

    for(int i = 0; i < 99; i++){
        
        rayPos = CameraPos + (RayDir * depth);
        dist = distanceFunction(rayPos, 1.0);
        
        if(dist < 0.0001) break;
      	  
        depth += dist;
        
    }


中身を説明します。

レイのポジション
rayPos = CameraPos + (rayDirection * depth); 

ここはレイのポジションをvec3のrayPos変数に入れています。
"depth"がレイが進んだ距離なのですが、これはあくまでfloatの数字なので、これにレイの"方向"を入れてあるrayDirectionを乗算してあげることで、
「その方向にRayがこれだけ進んでるよ」という情報になります。
それを、CameraPosに足してることで、
「カメラの位置からこの方向にこれだけRayが進んでるよ!今ここだよ!」という情報になるわけです、この情報を、rayPos変数に入れています。

オブジェクトまでの距離
dist = distanceFunction(rayPos, 1.0);

ここでは、distanceFunctionに現在のレイの位置を渡していて、一番近い位置にあるオブジェクトまでの距離が返ってきます。

レイの行進が終わるタイミング
    for(int i = 0; i < 99; i++){

for文の最初、99はレイを進めるのを終わらせる回数です。オブジェクトの表面にぶつからなければ99回でレイを進めるのを終了します。
もしレイが何かにぶつかったらループを抜ける、というのをif文で入れています。

        if(dist < 0.0001) break;

距離関数から返ってきた値(一番近い位置にあるオブジェクトまでの距離)が、0.0001より小さければ、「ぶつかった」と判断してここでループを抜けます。


ちなみにこの図↓だと何にもぶつからないので、99回ループしたところでレイを進めるのが終わります。
f:id:maplesyrup-cs6:20190705175247p:plain


ぶつかったらループを抜けてそこに色をつけるし、ぶつからなければ、空っぽく青くする、みたいなこともできます。

レイを進める
depth += dist;

最後にdepthにdistを足すことで、そこから先にレイが進んでいきます。

f:id:maplesyrup-cs6:20190705184704p:plain
depthにdistをたす


オブジェクトに色をつける
    if(dist < 0.0001){
       
        col = vec3(1.);

    }

レイがオブジェクトにぶつかったら、レイを進めるのを終了してぶつかったところに色をつけます。
今回は白に塗るので、vec3(1.0)をcolに入れています。



なんかややこしいのですが、最初はスフィアトレーシングをしてぶつかってるやつもどんどん色塗っていくのかなとか思ってたんですが、そうじゃないみたい。
スフィアトレーシングは「レイをどれだけ進めたらいいか」の指標として行っている、という感じです。
ある時点でのレイの位置から一番近いオブジェクトまでの距離分進むということは、絶対にその間に他のオブジェクトがないということだからです。


ということでこれで今回分の解説は以上です!🐼


続きは次の記事で!!

次の記事では今回レイマーチングで出したスフィアに立体感をつけたり色をつけたりします!
次の記事はこちら↓↓↓
(書いたら貼り付けます)