Unity板ポリレイマーチング入門
日本全国のUnityレイマーチングファンのみなさん!こんにちはこんにちは!!!!
8月も終わりですね!それでも熱い日々はまだまだ終わらないと思います!!(誤字ではない!!!)
この熱い勢いのままに、Unityでレイマーチングをやっていきましょう!関連性がない?!そんな細かいことは気にしない!!!熱い!!
この記事「Unity板ポリレイマーチング入門」はUnityゆるふわサマーアドベントカレンダー 2019の最終日の記事として作成されております!
https://qiita.com/nkjzm/items/7c933bb5c772a0b75512
前日は青木ととさんによる🥞Unity Designer's Cafeという勉強会の紹介とふりかえりでした。
それではレイマーチングをやって参りましょう!
先にコードのリンクを貼っておきます。
折り畳みのレイマーチング
カラフルな球
Unityの板ポリでレイマーチングを描こう
GLSLでレイマーチングを描く人たちのように、Unityでレイマーチングを試してみましょう!
世の中にはレイマーチングの入門記事がたくさんありますが、それらは見なかったことにして基本的な手順を説明していきます!!世界に入門記事はどれだけたくさんあっても構わない!!!
このコード全文はこちら。
折り畳みのレイマーチング
下準備
新しいシーンを作成し、Quadを配置します。Positionは(0, 0, 0)にします。
この四角形がカメラに映るように、Main CameraのPositionは(0, 0, -1)くらいに調整します。
レイマーチングを描くためのシェーダーを用意します。アセットにUnlit Shaderを作成します。
作成したシェーダーを右クリックし、マテリアルを作成するとそのシェーダーが適用されたマテリアルを作ることができます。
マテリアルをドラッグしてインスペクターのQuadに持っていくと下準備は完了です。あとはシェーダーを描いて*1保存するたびに、Unityエディタの板ポリに絵が出るようになります。
レイマーチングを行うために座標を調整する
Unityでシェーダーをダブルクリックするとエディタで開かれます。
シェーダーファイルの内容で、板ポリのレイマーチングに使うのはフラグメントシェーダーだけで、頂点シェーダーは変更しません。エディタで下の方にあるfrag関数に手を加えていきますが、関数を追加する場合はそのすぐ上に描き加えます。
デフォルトのUnlitシェーダーは一枚の画像ファイルを楽に扱いやすい状態になっています。
uv座標は四角形の左下が(0, 0)、右上が(1, 1)になります。
この原点座標(0, 0)を四角形の真ん中に変更します。これはレイマーチングで良く行われるやり方です。軸を中心に回転させる場合や、負の座標をabsやmodで折り畳んでオブジェクトの繰り返し表示を行うのに都合が良いんですね。
色の変数を変更する
レイマーチングはなかなか絵が出ないものです。まずは辛抱強く写経していきましょう。
色を格納する入れ物を変えていきます。
フラグメントシェーダーでのお絵描きでアルファ値は扱いません。ですので、RGBだけを変数として使いやすくするため、float3の入れ物に変更します。
RGBすべてをゼロにした状態でシェーダーを保存し、いったん絵を確認してみましょう。このフラグメントシェーダーはfixed4型をリターンすることで絵が出ます。
黒い板ポリが見えるようになりましたか?
もしエラーがある場合はピンク色になります。
シェーダーを選択している場合はインスペクターにエラー内容と行番号が表示されます。あるいはコンソールを開いてログを確認しても良いでしょう。
該当の行を修正してエラーが無い状態にします。
色を変えてみることにしましょう。ここでは色のRGに座標XYを設定しています。
板ポリにグラデーションが表示されました。何が起きているのでしょうか?
これは座標を色で見るやり方です。
板ポリの座標は左下が(-1, -1)、右上が(1, 1)です。右にいくほどにuの値が増えるので赤くなり、上にいくほどvの値が大きくなるため緑に近づきます*2。
色が出せることがわかったので、板ポリは黒い状態に戻します。
float3 col = float3(0, 0, 0);
カメラとレイを定義する
ここでコードのコメントを確認しながらカメラとレイについて見てみましょう。
レイを定義する行を追加しました。
奥行きのzが増えていることに注目してください。
レイの初期位置roを原点よりも手前(zがマイナス)に定義しています。この場所からレイはrdで定めた方向に進んでいきます。
レイの進むxy方向をpにセットします。そして奥行きのzは1です。レイは出発地点から板ポリの全ピクセルに向けて大量に発射されるようなイメージを持ってください。
方向だけをあらわすベクトル(大きさを扱わないベクトル)は、normalize関数で正規化することを忘れないようにしましょう。normalizeを書き忘れて絵がまったく出ない状況は結構ありがちです。これはレイが進み過ぎて衝突しないためです。絵が出ない場合にはnormalizeを書き忘れていないかを確認しましょう。
球を出す
マーチングループを描いていきましょう。
レイは最大でマーチングループで決められた回数だけ前に進みます。
何行か書き足してみると円が表示されました。実はこれは球です。
マーチングループの回数は100回や200回程度のループ数にするとおおむね良いでしょう。少なすぎるとディテールが出なくなり、多すぎると動作が重くなります*3。
球を計算するところをmap関数に切り出します。map関数の内容は続きの下にある内容を確認してください。
ライティングしてみる
法線を算出するnormal関数を追加します。
この関数は呼び出し先のmap関数よりも下に描かないとエラーになることは覚えておきましょう。
衝突したタイミングで法線を求めます。
でも、これをどう使えばいいのでしょうか?
ライティングは光源からのベクトルと、マテリアル表面からの法線ベクトルの角度の関係から明るい部分と暗い部分のグラデーションが決められます。これには内積が利用できます。
ここではポイントライトの光源を特定の座標に配置したものとして、ランバートでライティングを行います。
ランバートでライティングしてみます。1/dを使ったものは明るすぎるので、コメントアウトします。
別のライティング方法も見てみましょう。これはレイマーチングならではのフォグとAO(アンビエントオクルージョン)です。
レイが進んだループ回数を、最大ループ数で割ると0.0から1.0までの値が出てきます。
もしレイが衝突しない場合は真っ白になります。レイマーチングで遠景にグローがあるルックはこれを使っています。
また、カメラ位置から遠く離れるに従ってオブジェクトが明るくなり、徐々に遠景に溶け込んでいくようなフォグの効果もあります。
AOの効果を確認するには、もっと複雑な形状を作るのが良いでしょう。オブジェクトの凹凸が多ければ、AOのルックが作られることがわかります。
AO実装はヘミスフィア(半球)でレイトレースすると実現できます。マテリアル表面の一点から外側の180°方向にレイを飛ばし、衝突した遮蔽物がどれだけあるかを判断すれば暗くする度合いを決めることができます。
レイマーチングでループ回数を使ったAOは、衝突するまでにループ回数が多い場合は近くにオブジェクトが何かあるという扱いにできます。
ここで実装のコメントと見比べると、このコメントが実は嘘であることがわかります。衝突するまでにかかったループ回数が多いほど明るくなるので、フォグとしては機能しますが、AOとは逆の働きをしているのです*4。
球以外のものを出す
形状を扱いやすくするために関数を分けてみましょう。ここでは球の距離関数をsphere、箱の距離関数をboxとしています。
map関数から箱の距離関数を呼び出して表示してみましょう。
正方形が表示されました。実はこれは立方体です。
回転させる
回転させてみます。
Y軸周りに立方体を回転させます。回転の操作は、座標に対して回転行列の掛け算を行うと実現できます。ここでは座標と角度を与えると、回転後の座標を返すrot関数を用意しています。
繰り返しや折り畳みを使って複雑な形状にする
座標を加工します。
座標のabsを取ってから引き算をし、回転するという操作をループで何回か行うと複雑な形状が生まれます。
別の回転のやり方も見てみましょう。これはpolar mod、あるいはfold rotateと呼ばれるテクニックです。
空間を折り畳んでいくと、数回のループで簡単に複雑な形状を安価に作ることができます。
カメラを調整する
FOVを導入する
ここまではFOVを1.0で扱ってきました。これを変えてみます。
1.8
0.5
レイマーチングでは画面の情報量が多いほどルックが向上しがちです。
魚眼ルック
FOVにdotを使うと、画面全体を丸くひずませる効果を表現できます。レイを飛ばす方向を球状にするということですが、どんなルックになるかは実際に実装し、パラメータを変えて確認してみてください。
fov = dot(p, p);
fov = 1.0 - dot(p, p);
LookAtを設定する
カメラを制御できるように変更してみましょう。
ここまでカメラ位置を固定していましたが、カメラを動かすことができます。
カメラに対して前後、横、上下方向それぞれの軸を求めてあげることで実現します。三次元空間をXYZ軸で表現するように、カメラで3つの軸を決めていきます。
ポイントは、ふたつの軸ベクトルで外積を使うとその両方に垂直なベクトルを求めることができるということと、方向だけをあらわす(大きさは考えない)ためnormalize関数を使って正規化するということです。
カメラを動かしてみる
カメラはtargetを向くようになっているので、パラメータを変更して面白い動きのルックが作れます。
光源ベクトルにカメラ座標を利用して定義する
ここまでは光源を特定の座標に固定で配置したものとして扱ってきました。
ところで、レイが衝突した座標とカメラの座標がわかっていますので、カメラを光源としたスポットライトのようなライティングも実装できます。
HSVを使って色を扱う
ここまでモノクロで画面が寂しかったので、カラフルに着色してみます。
実装をわかりやすくするため、以下記事を参考にベースとなるレイマーチングを新しく用意します。
ブログ記事の実装を魔改造するとこんなかんじです。
追加した着色やライティング、カメラの動きについて順に解説していきます。
ここからは、世の中にあるレイマーチング入門記事の写経をしてはみたけれど、次に何をすればいいのか良くわからない人向けの内容です。
コード全文はこちら。
カラフルな球
HSVを使って色を乗せてみます。
HSVを利用すると、画面全体をカラフルにしたり、虹のようなルックを実現できます。
ただし、うかつにHSVをとりあえず使うだけだと、赤・青・緑とかひたすら紫・黄色といった割と汚いルックにもなりがちです。レイマーチングでの使い方には結構熟練が必要なのです。
とりあえずはパラメータh*6に何かを与え、sとvには1を与えてどうなるかというところから試していきましょう。何事も慣れが必要です。
今回は与えられた座標をもとに、hに与える値を作り出してみます。
考え方はマンハッタン距離をベースとして、それぞれの球にインデックスを割り当てます。xyzそれぞれの値を加工してfloorかceilで整数の値をうまく作り出していきます。
インデックスindはグローバルのfloatで宣言しています。
球を取り囲む見えない範囲、バウンディングボックスのようなものがあると想像してみてください。ここではceilでこのバウンディングボックスの範囲を作り出しています。
掛け合わせるマジックナンバーを変更してルックがどう変わるか試してみてください。バウンディングボックスがズレると、球の半分の箇所だけ色が変わったり、逆に連続するいくつかの球が同じ色になったりします。
hにインデックスを与えます。h、s、vそれぞれの値はルックを確認しながら調整してみましょう。
球ごとに色 が変われば実装はOKです。
法線と光源からのベクトルを使ってランバートで陰影を加えます。
正規化されたベクトルで内積を取ると値の範囲が-1.0から+1.0となりますが、負の値をレイマーチングで扱うやり方はいくつかあります。この実装では厳密なPBRを求めてはいないので、シンプルにmaxで0よりも大きい範囲に値をおさめています。他にもpowを使ってもいいでしょう。
hsvの色にランバートを積算していますが、加算で調整することもできます。
フォグ
レイが衝突するまでの回数をライティングに利用するのは、フォグまたはAOをシンプルに実現するやり方です。
ループが進んだ回数をループ最大回数で割れば0.0から1.0までの範囲の値を作ることができます。
この母数は ルックを調整した結果の値です。
SSS
シンプルなSSSの実装を加えてみます。これはRevision2019のシェーダーショウダウンで優勝したNuSanによるコードを使ったものです。
SSSは蝋燭や肌の質感を表現する手法として知られています。肌の奥には毛細血管や脂肪があり、そこで光が乱反射し、光が入ってきたのとは別の場所から出てくるためにマテリアルごとに質感が変わって見えるものです。
このSSSはシンプルです。衝突したレイをさらに先にいくらか進め、そこから近傍のオブジェクトへの距離を求めることでマテリアルの厚さをはかっています。この数値を加工してライティングに加えることで、透過しているような効果を加えています。
この形状はシンプルなため、あまりSSSの効果がありませんが、複雑に入り組んだ形状を作った場合には効果的です。特にマンデルブロやpmodを使ったレイマーチングとSSSは相性が良いので、試してみるのも良いでしょう。
カメラを動かす
カメラに動きをつけて面白いルックを実現してみましょう。
レイマーチングでカメラを動かすのはシンプルなコードで実現できます。単純なやり方から始めて、複雑な動きを作ってみましょう。
とりあえず前に進めてみましょう。カメラ座標のzに時間から作った値を加えると、画面が前に進み始めます。
float3 CameraPos = float3(0.0, 0.0, _Time.y);
何も動きがないよりは等速直線運動で前に進むだけでも十分な効果があります。
次は回転させてみましょう。
2次元の回転行列か、あるいは回転を行う関数を用意してあれば、あとはそれを使うだけです。
カメラをY軸まわりに回転させてみましょう。ポイントはカメラのx、zの初期値をゼロ以外の値にしておくことです。
CameraPos.xz = rot(CameraPos.xz, _Time.y);
これはRevision2019のシェーダーショウダウンでevvvvilが実装したカメラを元にしています。
ある程度の大きさの数値にsin(_Time.y)を掛けた値をカメラのzに使っています。
この実装ではカメラは原点をターゲットとしてそちらを向くようになっています。そのため、zがゼロになる前後でぐりっと向きが切り替わって面白いカメラ動作が実現できます。
このコードはglslfanに公開してあるので、どんな動きになるかは以下のリンクで確認してみてください。
いかがでしたでしょうか。
レイマーチングの入門から一歩先に進むための内容を主に取り上げてみました。この内容が熱い季節を乗り越える一助になれたら幸いです。
レイマーチングを描いたらぜひTwitterなどでつぶやいてみてください。ついでにこの記事のリンクも貼ってもらえると筆者が喜びます。
ではでは、良いレイマーチングライフを。
*1:シェーダーは画材なので、書くではなく描くという動詞が妥当でしょう。
*2:マイナスについてはゼロとして扱われます。なので、黒で表示されます。
*3:パフォチューする方法も世の中にはありますが、本稿では扱いません。
*4:これについての対策までは本稿では扱いませんが、ループ回数が上限に達した場合での分岐を入れるとフォグとAOを両立できるようになるでしょう。
*5:回転行列を用意してmul関数で掛け算をしても良いです。ただ、GLSLで*を使って実装するのと違いが出てしまうので、言語の違いによらず近い実装ができるように筆者はこのやり方を好んで使っています
*6:色相です。今日はこれだけを覚えて帰ってください。