vvvvでレイマーチング

この記事は vvvv Advent Calender  13日目の記事です。


みなさん、レイマーチングってご存知でしょうか?

vvvv使いの方にはちょっとなじみが薄いかもしれません。

そのうえ、ゴリゴリとシェーダーを書くことになるのでちょっと敷居が高いところもあると思います。

でも、ポリゴンとはまた違った形で面白い形状を扱うことができるので、試してみると面白いと思います!

レイマーチングとは


レイマーチングとは、3D空間内で光をトレースする、いわゆるレイトレーシングの一種です。その名の通り、光(レイ)を行進(マーチング)させることからその名前が…

というようなテンプレ説明はここで私がするより、WebGL界でとても有名なdoxasさんの記事あたり(これとかこれとか)を読んだ方がしっかり理解しやすいと思います。

なので、とりあえずここではざっくりと、簡単・高速にレイトレーシングするためのものくらいに言っておきます。

 

さらに、レイマーチングといってもまた様々に種類があったりするのですが、今回は「単にレイマーチングと言ったらコレ」くらいにあたりまえなスフィアトレーシングを使います。

 

環境


vvvv_50beta37_x64

DX11

 

32bit, DX9でもできるとは思いますが、シェーダーを扱うのであればDX11の方が表現の幅も広く便利だと思うのでそちらをお勧めします!

 

とりあえず球を出してみる


ものは試しです。とりあえずやってみましょう。

 

まずは、いつものようにパッチをダブルクリックでノード検索のウィンドウを表示し、“Template (DX11.Effect)”ノードをクローンしましょう(検索内のノードをCtrl+クリック)。

適当に名前と保存場所を指定して、クローンします。

配置されたシェーダーにQuad (DX11.Geometry)をつなぎ、Size XYを2にして、Renderer (DX11 Temp Target)をシェーダーにつなぎ、Target Format”R16G16B16A16_Float”の浮動小数点フォーマットに指定します。理由は後程説明します。

 

レイを定義する

さて、ここからが本番です。Templateのシェーダーを書き換えていきます。

まず、シェーダーの中のPixel Shaderに値する部分、このTemplateだと”PS”という関数がそれにあたります。

一応説明すると、一番下のtechniqueのSetPixelShader()の部分で指定されている関数がPixel Shaderとして動作するので、ここで指定されているPSという関数がPixel Shaderになります。

それでは、PS関数の中身を下のものに書き換えてみます。

すると、このような結果になると思います。

随時、Pipet (DX11.Texture 2D)をつなげて結果を確かめてみてください。

 

ここで何をやっているのかというと、レイマーチングの際に使用するレイを投げる方向を定義しています。

まず、最初のrayDirという変数ではQuadのUV座標を0~1の範囲から-1~1の範囲に正規化することで、画面から見た縦横の方向をベクトルにより定義しています。

最後にYを反転しているのは、ここではUV座標が-Yupになっているので反転させて+Yupにしています。

 

次に、rayという変数では、z(奥行き)方向を加えた最終的にレイを投げる方向を定義しています。XY(縦横)方向は、先ほど定義したrayDir変数により指定し、奥行きの方向は、常に画面奥に向かってレイを投げることになるため、一律で1としています。

こうして出来たベクトルを、空間内で正しく扱うために長さを1にする処理(正規化)normalizeによって行います。これにより、最終的なレイの定義は終了です。

ここではfloat4の変数に、定義したレイの方向を、そのまま色情報として出力しています。Pipetによって大体それっぽい感じに出ていることがわかってもらえるかと思います。

 

ここで、前に、RendererにてTarget Formatを浮動小数点フォーマットで指定しなければ、負数が扱えないので、0以下の値はすべて0で表示されてしまいます。なので、Rendererの結果で負数を含めたデバッグをとりたい、もしくは浮動小数点でテクスチャを次のパイプラインへ送りたいときは、Target Formatを16bit、もしくは32bitの浮動小数点フォーマットで指定しましょう。

はい。なので、ぶっちゃけこれ以降浮動小数点フォーマットが意味をなすところは多分出てきません。これが言いたかっただけです。

 

今回は、QuadのUV座標を利用していますが、中心を0として-1~1の範囲で正規化できれば、頂点座標でもデバイス座標等でも特に問題ありません。

ここまでだと、だったらTextureFXの方がパッチもすっきりしていいんじゃない?と思われる方もいるかもしれません。実際そうなんですが、今回はこれまた後述の理由により、DX11.Effectのまま行きます。

 

球を出してみる

いよいよ球を出してみます。

シェーダーに次のdSphere関数を追加し、さらにPixel Shaderを書き換えてみます。

ここまでシェーダーを書き換えると、次のような結果が出てくると思います。

おめでとうございます!これでレイマーチングで球がレンダリングできました!!

と言われてしっくりくる人はあまりいないと思います。

それはさておいて、コードの方を見てみます。

 

最初に追加したdSphereという関数はどういうものかというと、この手のものは距離関数(distance Function)といって、これがオブジェクト(球)を表すためのものになっています。もっというと、現在位置からオブジェクトまでの最短距離を返す関数です。引数は、その名の通り、rayPositionはレイの現在の位置。radiusは球の半径になります。この球を表す距離関数は割と直感的で、理解しやすいと思いますが、正直距離関数で直感的なのは球くらいで、世の中のほとんどの距離関数は直感ではほぼ理解不可能なのであまり考えなくていいと思います。

これをどのように使ってレンダリングしているのかというと、距離関数を使用し、現在のレイの座標からオブジェクトまでの最短距離を算出し、その最短距離分レイを少しずつを進めることによって、衝突判定を行います。

巷でよく見る感じの図を使うとこういうことです。

ポイントは、レイの進む方向に、距離関数により計算した最短距離分進んでも、そこにオブジェクトがあるとは限らないことです。図においても、最短距離分を6回進んでようやくオブジェクトと衝突しています。そのため、もとから最短距離を複数回進むことを想定しなければいけません。

そのように、レイが衝突するまで、距離関数による最短距離分を何度も進むための処理を行っているのが、Pixel Shader内のFor文の部分であり、俗にいうマーチングループです。

マーチングループに入る前では、必要な変数の初期化を行っています。レイの初期位置のzが-2なのは、単純にある程度離さないとカメラがオブジェクトの内部に定義されてしまうためです。

マーチングループ内では、レイが衝突しているかの判定を行った後に、距離関数による最短距離分(長さ)に、レイの進む方向のベクトルである変数ray(向き)を掛け合わせたものをレイの現在位置に足すことでレイを進めています。

このあたりの概念はKazuya Hirumaさんの記事が大変わかりやすくお勧めです。でも正直、詳しく説明されても一回で理解できる人ってそうそういないと思います、私もそうでした。なので最初はとりあえず雰囲気で進めていってもいいと思います。

このコードでは、衝突した場合は白、そうでなければ黒というようなとても単純なシェーディングですのでちょっとつまらないかもしれません。なので、ここからシェーディングを追加していきます。

 

法線の取得

シェーディングを行うためには法線(面の向き)が必要になります。距離関数を使うと、容易に法線を得られます。シェーダーに次のgetNormal関数を追加し、Pixel Shaderをまたまた書き換えて、今度は取得した法線を表示して見ます。

こんな感じになったと思います。ここでもPipetを使って随時値を確認してみてください。ちゃんと法線が取れているのがわかると思います。

新しく追加した、getNormal関数で法線の取得を行っているわけですが、何をやっているのかというと、現在位置で計算した距離関数の値から、現在位置からほんのちょっとだけずらした位置から計算した距離関数の値を引くことで、勾配を求めています。そして、この勾配を正規化すると、そのまま法線として使えるような仕組みになっているのです。便利ですね。

 

それでは、法線を使用して実際にシェーディングしてみます。今回はPixel Shaderの変更のみです。

シェーディングがついて、そろそろ球をレンダリングしている気になってきたと思います。ここでは詳しく言いませんが、今回はランバート反射というとても簡単な照明モデルを用いてシェーディングを施しています。ライトの位置に関してはシェーダー内で直接定義していましたが、これをvvvvらしくパラメータ化して外へ公開すると、パッチ上でインタラクティブにライティングを変更することができます。

 

ここまで来たら大体レイマーチングはわかったようなものです。様々な種類の距離関数を試してみましょう。

modeling with distance function

ありがたいことに、メガデモ、レイマーチングの神であるiq氏のページに主要な距離関数が大体まとめられてます。リスペクトを忘れずにありがたくコードを拝借しましょう。

Box

Torus

Cylinder

 

カメラを動かす

はい。そうですね。言いたいことはわかります。カメラが動かないと3D感が出ないですね。というわけでカメラを動かしてみます。

Pixel Shaderに対して、カメラを定義する変数をいくつか追加し、それによってレイの定義も変更します。

カメラの位置を表すcPosの値は、レイの初期位置に代入し、cSide, cUp, cDirは、それぞれカメラの向いている方向の横(x)、縦(y)、奥行(z)を表します。これを、レイの方向の定義の段階で、掛け合わせてやることで、レイを投げる方向を変えています。要するに、カメラの位置から、カメラの向いてる方向にレイを飛ばすように定義しているわけです。

カメラの定義に使用する変数は、パッチに公開しているため、これでパッチの側からインタラクティブにカメラの変更ができるようになったと思います。

 

いままででしっかりとvvvvに慣れている方ならこう思うと思います。

「vvvvには便利なカメラモジュールがたくさんあるのになんでこんなことしなくちゃならないんだ…」

ご安心ください。一応、一通り体験してもらおうと回りくどいことをしてましたが、ちゃんとvvvvカメラに対応させることもできます

まずは、現在の状態から、お好きなカメラモジュールを出して、いつも通りViewとProjectionをRendererにつないでみましょう。

はい。たぶん変な感じになったと思います。原因はお分かりだと思いますが、この状態だとカメラモジュールで視点が変わることによって、いわゆるキャンパスの役割をしていたQuadの見え方が変わってしまいます。さらに、よく見るとレイマーチングで描画しているオブジェクトは全く変化していないと思います。当たり前ですが。

これを防ぐには今までと違い、今度はVertex Shaderを変更します。Pixel Shader同様、techniqueで指定されている(今回はVSという名前)関数に手を加えていきます。

変更するのはたった1行です。なんか2個ついてたmulとかいう関数を外してあげるだけです。というのも、このmulという関数は、ベクトルや行列に対して行列を掛け合わせる(Multiply)ための関数であり、ここでは、カメラによって生成されたView行列Projection行列を頂点座標に掛けることによって、クリッピング空間へ座標変換を行っている部分です。シェーダーに慣れていない人には聞きなれない単語がたくさん出てきたと思います。が、これを解説しようとすると途方もないことになるので、なんかカメラに合わせて見え方変えてる場所くらいの理解でいいです。

カメラに合わせて見え方変えてる場所を変えずにそのまま出力するように書き換えたので、Quadがカメラに反応しなくなったと思います。

このまま、レイマーチングのオブジェクトの方に対応させていきましょう。

まずは、カメラ関連の行列の宣言を行っている個所に、2つの宣言を追加します

vvvvっぽくtVI,tPIと書きましたが、これは、カメラから送られてくるView行列Projection行列それぞれの逆行列です。逆行列とは、定義的には元の行列と掛け合わせたときに単位行列になるような行列のことです。座標変換においては、元の行列と逆の変換を行う行列となります。vvvvのHLSLではいくつかvvvv側で定義されたセマンティクスがあり、変数名に対して特定のセマンティクスをつけることによって受け取れます。ここではVIEWINVERSEPROJECTIONINVERSEというようにセマンティクスを指定するだけで、勝手にカメラ行列の逆行列を計算して設定してくれます。ホント便利だと思います。さっきのTextureFXを使わない後述の理由とはこのことで、このあたりのセマンティクスの使用は、TextureFXでは「基本的には」できないことなので、カメラを簡単にぐわんぐわんしたければDX11.Effectを使用することをお勧めします。

そして、これを使用してPixel Shaderを書き換えます。ちなみに、先ほどカメラの定義に使っていたcDir等は削除してかまいません。

さて、いよいよややこしくなってきました。

とりあえず、コードだけでも打ち込んでみると、確かにいつものvvvvの操作感でカメラが変更できるのが感じられたと思います。

では、実際コードでは何をやっているのか。変更箇所を上から見ていきます。

まず、さっき2個消したmulが代わりに(ってわけではないですが)2個追加されています。順番的には、ProjectionInverse→ViewInverseの順番で掛けています。

まずProjectionInverseをかけているところですが、これは、縦横の方向に対して補正をかけている処理です。そもそもProjection行列というのは、画角やクリッピングなどの定義が含まれる行列なので、これを掛けることで画角などの変化に対応させています。ではなぜ逆の行列をかけるのか?それは、これがレイ、つまり世界に対する処理だからです

通常、Projection行列というものは(Viewもですが)オブジェクトの頂点に対してそのままの状態で掛けるものです。オブジェクトに対する変化は、世界に対して逆の変化を適用することでも再現できます。オブジェクトをx方向に1ずらすのは、代わりに世界(というか空間やカメラといった方が理解しやすいかもしれません)の方をx方向に-1ずらすのと見た目は変わらないですよね?同じことがここでも言えます。レイの開始点を上にずらすとオブジェクトが下にずれるように、通常オブジェクトに施すのとは逆の処理をレイに対して行うことで同様の結果が得られます。頂点座標を持たない距離関数オブジェクト特有の処理ですね。ここでは、縦横の補正があればいいので、計算後の結果のうちのxyのみを使用しています。

かなり雑な説明でしたねすいません。これで理解してもらえるかわかりませんが、わからなければレイに対してカメラの逆行列をかけるとそんな感じの効果が得られるっぽいって感じでいいと思います。

次にViewInverseをかけているわけですが、たぶんこちらの方が上の例をとってみるとわかりやすいと思います。View行列は、カメラの位置や向きなどの定義が含まれるので、それの逆をレイに対して掛けることで、オブジェクトがカメラに合わせて動いているように見せています。ここで、zの1はレイを投げる方向です。今までもそうでしたね。

掛ける順番についても、通常はView→Projectionの順番で掛けるので、その逆のProjection→Viewの順番で掛けていることになります。

その下のレイの初期位置を定義している場所ですが、この構文は、ViewInverse行列の4行目(float4)の最初に3つの要素である、xyz成分を取り出すという意味になっています。なんてことはないです。ここにカメラの位置情報があるというだけの話です。View行列の逆行列のこの場所に、カメラの位置の情報があるというのは、覚えておくと使い道があるかもしれません。

 

さて、かなりヘビーでしたが、ここまでついてこれた方はカメラに合わせて自由に動くレイマーチングを手に入れているはずです。ここまでくればこっちのものです。

ここからはお楽しみです、距離関数の特性を生かしたの形状の定義を見ていきましょう。

合成、くりぬき、共通部分

 

スムースな合成

大体同様の方法でくりぬきなどもできます。

 

Repeat

なんか、球の端っこが黒くなるとか、描画される球が少ないと感じたら、それは単純にループが少なくてレイがその場所まで届いてないということです。マーチングループの回数を増やしたりしてみましょう。ここで、fmodがあるのに剰余の計算(mod関数)をわざわざ定義しているのは、HLSLのfmod関数が-側に対してリピートをしてくれないためです。

 

3Dフラクタル

立体感をわかりやすくするためにAmbient Occlusion(環境遮蔽項)を入れています。距離関数内のループの回数を増やすとどんどん再帰的に穴が開いていきます。これを上にあるリピートで繰り返すととても面白いです

 

すごいですが、重いです。

 

この辺りも、前述のiq氏のページにたくさんの種類が載っています。たくさん試してみましょう。

 

ポリゴンと同居する


距離関数は、その性質上、幾何学的な形状の定義にとても優れていますが、ポリゴンモデルのような有機的な形状は不得意です。

なら得意な部分はポリゴンにやらせればいいよね。ということで、深度値を考慮した、ポリゴンのオブジェクトと同居した描画も挑戦してみましょう。

今回は書き換える部分が多いです。

まず、シェーダー内のどこか(Pixel Shaderよりも上)にpsoutという構造体を定義します。

そして、Pixel Shaderを書き換えます

ここまで書き換えた状態で適当に距離関数を定義し、パッチ側ではteapot+PhongDirectionalから出力されたLayerデータをGroupノードでまとめてRendererにつなぎます。

いかがでしょうか?交わりそうもないものなポリゴンオブジェクトとレイマーチングのオブジェクトが見事にいい感じに深度を考慮した描画がされていると思います。

感動しませんか?私はしました。

どういう仕組みなのかというと、Pixel Shaderの方で、色情報であるSV_Targetと一緒に、SV_Depthを定義してあげることで、深度を上書きしています。本来はカメラの目の前にあるQuadの深度によって、teapotとQuadのzテストがされるはずですが、Pixel Shaderで深度値を上書きすることで、上書きした値を使用してzテストを行ってくれます。夢が広がりますね。

ちなみに、この深度値を上書きする方法もTextureFXからでは利用できないので、ポリゴンと一緒にレイマーチングしたいときはDX11.Effectを使うといいです。

深度値の上書きさえすれば

これの右上のやつのように、レイマーチングで描画したフラクタルの内部で、ポリゴンで描画したトライアングルのパーティクルが舞うというようなことも勝手にやってくれます。

 

 

ここまでの技術を使えばかなり可能性が広がることがお分かりいただけたと思います。レイマーチングはちょっと敷居が高い分、とてもユニークな描画が行えるほか、ほぼレイトレーシングなので、リフレクションや影の計算などを正攻法に近い方法で行えることも魅力です(重いですが)。

せっかく勉強してて楽しかったので、vvvv界隈にもレイマーチングが少しでも広まればなーと思って書いたので挑戦してもらえると嬉しいです。

今回のコードとパッチはここにおいてありますので。

 

最後に有用な学習リソースを提示して締めようと思います。

 

WebGL.org

言わずと知れたWebGL界の重鎮doxasさんの運営しているサイトです。GLの情報のみではありますが、レイマーチングが日本語でとても分かりやすく解説されています。何度もお世話になりました。

 

Inigo Quilez :: fractals, computer graphics, mathematics, demoscene and more

メガデモの神iq氏のサイト。Articleのページに、様々な有益なレイトレーシングに関する技法がまとめられている。

 

シェーダー芸人になりたかった6か月前の自分に教えてあげたいリンク集

今年のTokyo Demo Fest 2018のGLSL Compoで1st placeとったすごい方の記事。私も1週間くらい前にこの記事があってほしかった。

 

Shader Toy

世界中から、GLSLのPixel Shaderのみを使用したプログラムが集まり、実際に手元で実行しながら見れる。世界超ウィザード級のえげつないテクニックが所せまし。上級テクニックを身につけたくなったらここをあさるといいと思います。

 

Field Trip

ものすごいゴリ押しでvvvvノードによるレイマーチングオブジェクトの定義を実装しています。貴重なvvvvによるレイマーチングの実装なので参考になると思います。ほかにもContribution系だとこの辺りがいい実践例だと思います。

 

vvekend vvorkshops – raymarching basics

vvekend vvorkshopのレイマーチの回です。正直私まだ見てないんですが、こちらも貴重なvvvv直でのリソースなので役に立つと思います。

 

あとは、よくレイマーチングを使用して作品を制作していらっしゃるがむさんのたぐすさんFMS_CatさんなどのブログやTwitter等をチェックしておくととても有用な情報があると思います。

 

最後に

初Advent Calenderとしては大分ヘビーで、かつ初心者向けなんだかそうでないんだかわからない記事になってしまった感がありますが、ずっと書いてみたかったvvvvのAdvent Calender記事が書けて良かったです。それでは。

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です