はじめに
この記事は、レイマーチングを完全に理解する記事の続きです🐼
レイマーチングを完全に理解するにはまず最初にこちらの記事をご覧いただければと思います!
実際にやってみよう〜立体感を出そう〜
前回の記事ではここまで作りました🐼レイマーチングの第一歩ができました!
でもこのままだとなんだかのっぺりしていて立体感がないですね。
今度は立体感を出しましょう。
立体感を出すには?
立体感を出すには、
①法線を求める
②ライトの位置を決める
③法線とライトの内積を算出する
の3ステップです。
つまり、法線とライトの向き(ベクトル)をもとに、影を計算します。
①法線の取得
まずは法線の取得についてです。
オブジェクトはたくさんの面が集まってできていて、その面ごとに向いてる方向がありますね、その面が向いてる方向のことを法線と言います。
法線を取得するには、偏微分をします。正直に言います。私は偏微分がなんなのかわかりません。
デキる人に聞いたところ、ヘンビブンをするとその点の法線(垂直な線)が求まる、とのことです。
が、私はド文系で「内積」すらシェーダーを始めて初めて知ったレベルなので、同じレベルの人は一旦ここは”ホウセンハヘンビブンデモトメル”とだけ覚えておきましょう。理解したらまたまとめます。
vec3 normal(vec3 p, float size){ //法線の取得 vec2 e = vec2(0.0001, 0); float d = distanceFunction(p, size); vec3 n = d - vec3( distanceFunction(p - e.xyy, size), distanceFunction(p- e.yxy, size), distanceFunction(p - e.yyx, size)); return normalize(n); }
これがヘンビブンの式です。この関数にRayPosとSizeを渡すことで、法線の計算をしてくれて、vec3型のnを返してくれます。
ここで重要なのは、returnするときに normalize(n);が返されてることです。
求めた値をnormalize()することでベクトルの長さを1にして後々計算しやすいようにしてくれています。正規化と言います。
このヘンビブン関数の呼び出しは下の説明でやるので、この関数だけ付け足しておいてください。
②ライトの定義
次に、ライトの定義です。
前回の記事で定義したRayDirの下あたりに以下のlightDirを定義します。
vec3 lightDir = normalize(vec3(1.0, 1.0, -10.));
これは、ライトの場所の方向を決めています。ライトは太陽光をイメージしてください。
太陽光は360度に光が分散されているので、”ライト自体がどっちを向いてるか”は考えなくていいのです。
「太陽どの方向にある?」「あっちの方向!」という意味の方向を求めています。
例えば、真上にあるのであれば、
vec3 lightDir = normalize(vec3(0.0 , 1.0, 0.0));
といった感じです。
ここでまた重要なのは、正規化(normalize())しているところです。
ライトの値も正規化して、1にすることで、のちの計算をしやすくしています。
③法線とライトの向きの内積
「法線取得の関数」、「ライトの定義」ができたらいよいよ立体感を出していきます。
前回の記事で、オブジェクトに色をつけた方のif(dist < 0.0001)の中に書きます。
if(dist < 0.0001) { vec3 n = normal(rayPos, 1.0); //法線の向きの計算 float diff = dot(n, lightDir); //法線とライトの方向の内積 col = vec3(diff); //内積の値をそのまま色に反映 break; }
まず、ここで先ほどの法線の向きを計算する関数(ヘンビブンのやつ)を呼び出します。
変数 n に法線の値が入りました。
次に法線とライトの内積を求めるのですが、
その前になぜ法線とライトの内積を求めると立体感が出せるのかを説明をします。
まず、内積というのは、「二つのベクトルがどの角度で交わっているか」です。
例えば、以下の図でいうと、
ライトが真上にある場合、”ライトが上にある”ということに対して球のそれぞれの面の法線がどの向きを向いているか
を数値化する これを内積と言います。
法線もライトも正規化して値が1になっているので、内積を求めた時に−1から1の範囲で値が返ってきます。
なのでこの場合、二つのベクトルの向きが平行で同じ方向を向いていれば1、垂直に交わっていれば0、平行でかつ交わっていれば−1になります。
この内積の値が1に近ければ、ライトが当たっているということになるので、色を明るくし、
逆に0に近ければライトが当たっていないという判断ができるので、色を暗くすることで立体感のある絵を出すことができます。
話は戻り、以下、内積を求めるコードです。
float diff = dot(n, lightDir); //法線とライトの方向の内積
内積を出すのは、dot()関数なので、dot()関数に法線の向きとライトの位置を渡してあげることで、内積が求められてdiff変数に入ります。
ちなみにdiffはdiffuseの略で、「拡散反射」の意味です。よくわかんないけど私の師匠がこの名前にしてたのでこれを採用しました(ひでぶ)。
ここで返ってくる値、−1から1の値をそのまま色に反映させて立体感を出します。
col = vec3(diff); //内積の値をそのまま色に反映
この時点での全体のコードは以下の通りです。
float distanceFunction(vec3 pos, float size){ //距離関数 return length(pos)-size; } vec3 normal(vec3 p, float size){ //法線の取得 vec2 e = vec2(0.0001, 0); float d = distanceFunction(p, size); vec3 n = d - vec3( distanceFunction(p - e.xyy, size), distanceFunction(p- e.yxy, size), distanceFunction(p - e.yyx, size)); return normalize(n); } void mainImage( out vec4 fragColor, in vec2 fragCoord ) { // Normalized pixel coordinates (from 0 to 1) 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)); vec3 lightDir = normalize(vec3(1.0, 1.0, -10)); 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){ vec3 n = normal(rayPos, 1.0); float diff = dot(n , lightDir); col = vec3(diff); } fragColor = vec4(col,1.0); }
これでこういう絵ができたと思います。
これでレイマーチング最初の2歩ができました!🐼
発展編
レイマーチング第一歩ができたらそれに色をつけたり、数を増やしたりしていきましょう!
量を増やそう
まずは量を増やしてみましょう!
量を増やすには、mod()関数を使用します。
文系のあなた。mod関数ってなに〜?(ハナホジ)って思いませんでした?私は思いました。
mod関数っていうのは、なんか、ギザギザします!!!!(以下参照)
ちゃんと説明します。
modは余剰演算といって、割り算のあまりを算出する演算のことです。演算子「%」と一緒ですね!
y = xだと、線はまっすぐ登るか降りるかになります。
この値を、例えば2で割ると、
2 % 2 = 0, 2.1 % 2 = 0.1, 2.2 % 2 = 0.2, .......3 % 2 = 1, ..............4 % 2 = 0 というように、ちょっとずつあまりの数が上がって2上がったところであまりが0に戻ります。
元の数字がどれだけ大きくなっても「あまりの数」はこれを繰り返すので、ギザギザになります。
mod(x, 2.0); というコードだと、x軸の値を2で割ってそのあまりの数がy軸になるので、こういうグラフの見た目になるわけですね。
このmodの繰り返しの性質を利用して、オブジェクトを量産します!
ちなみに上の図のグラフは、GLSL Grapherというブラウザ上でできるGLSL用の数式可視化サービス(?)です。
「この数式でなんでこんな絵が出るだ?」ってなったときはこれを使うと理解できたりできなかったりします。
さて。ではmodを使っての量産コードを書いていきます。
距離関数の中身は変える可能性が高いため、mod関数を用いた繰り返し用の関数を作ってしまいます。
float map(vec3 pos, float size){ pos.xz = mod(pos.xz, 10.) - 1. *5.; return distanceFunction(pos, size); }
posのx軸とz軸を繰り返ししています。y軸も繰り返したいときは、pos.xyzにすればxyz軸全部繰り返しできます。
-1 * 5. は微調整です。ちなみにグラフでは、-1するとy軸が全体的に-1下がります。この辺の数字は色々変更して確かめてみてください。絵ではなんか変な感じになります。なんで変な感じになるのかいまいちわかってないです。
ここでDistanceFunction()を呼び出してるので、MainのなかでDistanceFunction()を呼び出していたところをmodを使用した関数「map()」の呼び出しに変えるのと、法線の計算をしていたnormal関数の中でdistanceFunction()をmap()に変更します。
これで実行すると!!!
こんな絵が出てきましたでしょうか🐼
真正面からみるとなんだかよくわからないので、斜め上からみてみましょう。
カメラのポジションを、Y軸を少しあげてみます。
vec3 CameraPos = vec3(0, 5, -10);
わあ!いっぱい増やせてますね!!
色をつけよう
色をつける2つの方法を紹介します🐼
①colに乗算してあげます。
if(dist < 0.0001){ vec3 n = normal(rayPos, 1.0); float diff = dot(n , lightDir); vec3 c = vec3(0.9, 0.4, 0.7); //色を作る col = vec3(diff * c); }
vec3型のcという変数に色を作っておいて、それを乗算します。
②mix関数を使う
2色とかでいい感じにしたいときはmix()関数を使うと良いっぽいです。
mix()関数はUnityでいうlerp()と同じで、線形補間の関数です。
mix(a, b, c);
という書き方で、
a:値1
b:値2
c:混ぜる割合(0.0 - 1.0);
となります。
例えば、2つ色を作っておいてそれをuvのx軸の値で混ぜると、uvのx軸は左端から右端までで0.0-1.0になってる(正規化してる)ので、0.0の地点と1.0の地点が値1と値2の色になり、その間はmix関数によって埋めてくれるため、グラデーションのような色合いになります。
これを利用して、法線と光の内積を混ぜる割合にして色をつけると以下のようになります。
if(dist < 0.0001){ vec3 n = normal(rayPos, 1.0); //法線を求める float diff = dot(n , lightDir); //法線とライトの内積を求める vec3 c1 = vec3(0.5, 0.4, 0.9); vec3 c2 = vec3(0.9, 0.4, 0.7); col = mix(c1, c2, diff); }
変数c1とc2に色を作ってdiffの値(法線と光の内積)でmixすると結果は以下のような感じに。
変更前(col = vec3(diff);)の時に影になってたところが、値2の色になってます。
動かそう
最後に簡単な動きだけつけて完成にします。
オブジェクトが大きくなって小さくなってってなるやつします。
シェーダーで動きをつけるには、Timeを使います。時間ごとに変化する値で動きをつけるって感じです。
今回レイマーチングして出したオブジェクトの値を変えるにはどうしたら良いか?
レイマーチでオブジェクトを描画するには距離関数を使用しました。その時に、値を二つ渡しましたね。
ポジションと、サイズです。そう、サイズ!
ここまではサイズに1.0という固定を渡していましたが、ここの値を変えてあげるとなんだか動きそうな感じがします!
ここ↓
dist = map(rayPos, 1.0);
距離関数につながるmap関数の第二引数がsizeなので、そこに値を渡します。シェーダートイなので「iTime」ですが、使用するエディタによって変わるので注意してください。
ここでは説明の統一のために「Time」と言います。
Timeだけだとだんだん値がでかくなってしまうので、反復?させる必要があります。
その際に使うのがサインです。
sin(Time);
これを、距離関数のサイズを渡してるところで使用して、
dist = map(rayPos, sin(iTime*2.)*2.);
これで時間が繰り返されます。
これで丸が膨らんだり縮んだりしました!
ただ、サイン波だと波ができて繰り返されてはくれるものの、マイナスの値までいってしまうので縮んだ後また膨らみ出すのに時間がかかります。(正確には、マイナスの値に膨らんでいる。)
それをなんとかしてくれるのが、絶対値というやつです。
サイン波の絶対値をとると、プラスマイナス関係なく値を見るので、以下のようなグラフになります。
これを組み込むとマイナスの値がなくなるので、縮んで0になったらすぐまた膨らみ出します。
絶対値はプログラミングでは abs() と書きます。
これを組み込んで、以下のようになります。
dist = map(rayPos, abs(sin(iTime*2.))*2.);
作り込むと長くなってくるので変数にしちゃうと良いです。
float size = abs(sin(iTime*2.))*2.; . . (中略) . dist = map(rayPos, size);
これで、本ブログの目標までたどり着きましたー!パチパチ🐼
全コードは以下の通りです!
float distanceFunction(vec3 pos, float size){ //距離関数 return length(pos)-size; } float map(vec3 pos, float size){ pos.xz = mod(pos.xz, 10.) - 1. * 5.; return distanceFunction(pos, size); } vec3 normal(vec3 p, float size){ //法線の取得 vec2 e = vec2(0.0001, 0); float d = map(p, size); vec3 n = d - vec3( map(p - e.xyy, size), map(p- e.yxy, size), map(p - e.yyx, size)); return normalize(n); } void mainImage( out vec4 fragColor, in vec2 fragCoord ) { //正規化 vec2 uv = (fragCoord*2.0 - iResolution.xy) / min(iResolution.x, iResolution.y); vec3 CameraPos = vec3(0, 5, -10); //カメラのポジション float screenZ = 2.5; //スクリーンポジション vec3 RayDir = normalize(vec3(uv, screenZ)); //レイの方向 vec3 lightDir = normalize(vec3(1.0, 1.0, -10)); //ライトの位置 float size = abs(sin(iTime*2.))*2.; //オブジェクトのサイズ 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 = map(rayPos, size); //一番近いオブジェクトまでの距離を測る if(dist < 0.0001) break; //オブジェクトに例がぶつかったらfor文を抜ける depth += dist; //レイの進んだ距離の更新 } if(dist < 0.0001){ vec3 n = normal(rayPos, 1.0); //法線の取得 float diff = dot(n , lightDir); //法線と光の内積取得 vec3 c1 = vec3(0.5, 0.4, 0.9); //色1 vec3 c2 = vec3(0.9, 0.4, 0.7); //色2 col = mix(c1, c2, diff); //色を混ぜる } fragColor = vec4(col,1.0); }