シェーダ内でレイを定義する
今回のサンプルの実行結果
ray marching の世界
前回は、GLSL だけを用いてリアルタイムにノイズを生成することに挑戦しました。
GPU の力をフルに引き出せる WebGL ならではの高速なノイズ生成は、今後も GLSL で様々な処理を記述していく上で必ず役に立ちます。若干シェーダのコードが多かったのでわかりにくい部分もあったかもしれませんが、落ち着いて考えてみればそれほど難しくないと思います。ぜひ、がんばって習得してください。
さて、今回からいよいよ GLSL でray marching(レイマーチング)を実装していきます。本テキストでは、そもそもレイマーチングとはなんなのか、そしてレイマーチングを行う上で欠かせないレイの定義について詳しく解説していきます。
サンプルとしては見た目に派手さはありませんが、今後のレイマーチングに関するテキストの基本中の基本となる部分ですのでしっかりと理解できるようにがんばってください。できる限り、知識が無い状態でも理解できるように詳細に解説したいと思います。
ただ、まったく 3D プログラミングの経験がないのに、いきなりレイマーチングに挑戦するとなると若干苦しい部分があることも否めません。もし、3D プログラミングの経験がまったくないままこのページに辿り着いた方は、多少面倒に感じるかもしれませんが、まずは通常の WebGL の実装から習得することをオススメします。
というのも、最初から脅すわけではありませんが個人的な見解としてレイマーチングは少々難易度が高いです。数学的にも、またその概念をつかむ上でも、あらかじめある程度の 3D プログラミングに関する知識があったほうが間違いなく有利です。ここは最終的には個人の判断になりますが、レイマーチングを正しく理解するためにこそ、予習は非常に重要だということだけ最初にお伝えしておきます。
さて、それでは早速次項よりレイマーチングについて詳しく見ていきましょう。
レイマーチングとレイトレーシング
さて、レイマーチングについて触れる前に、まずはレイトレーシングについて簡単に触れておきます。
レイトレーシングは、通称レイトレなどと略して呼ばれることもありますね。レイトレーシング、という言葉には非常に広い意味合いが含まれるので[ レイトレ=ある特定の技法 ]というように一言では言い表せない部分があります。しかし、一般にレイトレーシングというと、その名前が示すとおり[ レイ(ray)をトレース(trace)する ]という意味合いであり、要は[ レイを追跡する ]ということを指して広い意味でそのように呼ばれます。
レイを追跡するというふうに言われても、それでもまだピンとこない人も多いでしょう。
レイ(ray)は直訳すると[ 光線 ]などと訳されますが、レイトレを手っ取り早く理解する上では、光線よりも[ 視線 ]というふうにとらえたほうがわかりやすいでしょう。トレースは、先ほども書いたように[ 追跡 ]を表します。つまり、レイトレーシングとは視線を追跡してレンダリングする技法の総称であると考えるといいのではないでしょうか。
先述のとおり、レイトレとは一口に『これがレイトレだよ!』というふうには到底言えない幅広く奥深いものです。多種多様な技法が今現在も次々と考案されている非常に高度な分野でもあります。しかし、割と昔からある基本的な技法に限って考えれば、どうにもならないほど難しいものではないと思います。レイトレ自体に深い関心のある方には各自調べていただくとして、当テキストで登場するレイトレーシングという言葉については、あくまでも原始的な昔ながらの、基本的なレイトレーシングを前提に進めていきます。
さて、それではここで話をちょっと戻して、今回のテキストの肝でもあるレイマーチングとはいったいなんなのかについて考えてみましょう。
レイマーチングは、いわゆるレイトレーシングという分野におけるレンダリング技法の一種です。そう、レイマーチングも、広い意味ではレイトレーシングの技法のひとつなのですね。
レイトレーシングの概念
レイトレーシングは、レイを追跡して得られた結果を元に、スクリーンに何かを描き出します。やり方は様々ですが、基礎的な方法では視線(レイ)をレーザービームのようにまっすぐ飛ばして、オブジェクトと衝突しているかどうかを計算して割り出します。
図解すると以下のような感じでしょうか。
レイトレーシングのイメージ図
上の図で言うと、青い色をしたオブジェクトにレイが衝突している状態なのがわかりますね。このように、レイとオブジェクトの衝突判定の結果を元にして、衝突しているならオブジェクトをレンダリングしてやります。通常の WebGL のプログラムでは、行列を用いてカメラを定義してやり、空間を切り取ってレンダリングを行いました。しかし、レイトレーシングの場合は根本的に考え方が違っており、レイが衝突したか否か、これを指標にしてレンダリングを行っていくわけです。
しかしここで図式化して説明したのは、あくまでも基本的なレイトレーシングの考え方です。レイマーチングの場合は、ちょっと違います。
レイマーチングは、あくまでも前述のとおりレイトレーシングの技法の一種です。ですから、先ほど説明した方法と似たような方法を用いる場合が多いです。ここで[ 場合が多いです ]と書いたのには理由があって、レイマーチングにも、いくつかやり方が存在します。
いや、ほんとにややこしいですね(笑)
ここはひとつ、おちついて考えましょう。
- レイトレーシングにはたくさんの技法がある
- レイマーチングはそのなかの技法の一種である
- レイマーチングにもいろいろな種類がある
簡潔に言うなら上記のよう言えますね。
そして、レイトレーシングがレイをまっすぐ伸ばして衝突判定する方法だったのに対して、レイマーチングは、いったいどのようにレイを扱うのでしょう。
今回のテキストで紹介する方法は、一般にスフィアトレーシング(Sphere Tracing)と呼ばれているレイマーチングの技法です。スフィア、という言葉が含まれていることからもわかるとおり、球の概念とレイを組み合わせてレンダリングを行います。こちらも先ほどと同様に図解してみましょう。
スフィアトレーシング(Sphere Tracing)のイメージ図
ここで注目すべきは、赤い色で描かれている矢印です。
カメラからふたつの矢印が伸びていますね。ひとつは先ほどのレイトレーシングの場合と同じように伸びているオレンジの矢印。もうひとつ、赤い矢印が描かれています。この赤い矢印は、空間内に置かれているオブジェクトまでの、最短距離を表しているのですが図を見てわかるでしょうか。
カメラに最も近い場所にあるのは、緑色のオブジェクトですね。そして、この緑色のオブジェクトまでを直線で結んで最短距離を算出し、その最短距離を半径とする球体を作ります。これがグレーの円で表現されています。そしてグレーの球体の半径と同じ距離だけ、オレンジ色の矢印、つまりレイが進んでいるのがわかると思います。続けて、同じようにレイの先端部分から再度、最も近い距離にあるオブジェクトまでの最短距離(赤い矢印)を測ります。最短距離が計測できたら、その分だけレイ(オレンジの矢印)を進めます。
このように、オブジェクトまでの最短距離を指標として、レイの長さを徐々に伸ばしていく方法、これがレイマーチングの技法のひとつであるスフィアトレーシングです。
スフィアトレーシング
前項までで説明したとおり、スフィアトレーシングの場合にはレイを段階的に伸ばしていきます。レイを伸ばす単位は、空間内に置かれているオブジェクトまでの最短距離を指標としているのでしたね。
もし、レイの軌道上にオブジェクトが存在する場合には、計測される最短距離が非常に小さな数値になっていくことが想像できます。これは先ほどのイメージ図を見ても、なんとなく理解できると思います。スフィアトレーシングにおいては、この最短距離が非常に小さくなった時点で[ オブジェクトと衝突している ]とみなします。もしくは[ レイの先端が限りなくオブジェクトに近い状態 ]とも言えますね。
基本的なレイトレーシングでは、レイをまっすぐ伸ばして衝突しているかどうかを判断しました。レイマーチングでは、レイを段階的に伸ばしていき、最短距離が十分に小さくなった時点で衝突していると判断するわけです。
そして、レイトレーシングにしてもレイマーチングにしても、まず何をさしおいてもレイを定義しなければ話になりません。ここで言うレイとはすなわち視線のことであり、言い換えるとレイを表すベクトルでもあります。
当テキストでは、このレイの定義の方法をコーディングしてみましょう。
実際の衝突判定のやり方については、非常に長くなってしまうので次回以降にじっくり解説していきたいと思います。
シェーダ内でレイを定義する
通常の WebGL プログラミングでは、javascript からカメラの情報を行列に含ませるなどしてシェーダにプッシュします。しかし今回のように GLSL だけを用いてレイマーチングを実装する場合には、カメラやレイの定義はシェーダ内で直接記述します。
レイの X 成分と Y 成分についてはスクリーンの X 座標と Y 座標をそのまま用いて算出できます。そこに Z 成分、つまり奥行きに関する情報を付加してレイを定義します。
実際にシェーダのコードを見ながら考えてみましょう。
フラグメントシェーダでレイを定義する
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
void main(void){
// fragment position
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
// camera
vec3 cPos = vec3(0.0, 0.0, 3.0); // カメラの位置
vec3 cDir = vec3(0.0, 0.0, -1.0); // カメラの向き(視線)
vec3 cUp = vec3(0.0, 1.0, 0.0); // カメラの上方向
vec3 cSide = cross(cDir, cUp); // 外積を使って横方向を算出
float targetDepth = 0.1; // フォーカスする深度
// ray
vec3 ray = normalize(cSide * p.x + cUp * p.y + cDir * targetDepth);
// color
gl_FragColor = vec4(ray.xy, -ray.z, 1.0);
}
上記のコードのうち、最初の vec2
型変数である p
に値を代入するところまでは、今までの GLSL カテゴリのテキストで登場してきたやり方と同じです。ここがまず理解できないという方は GLSL カテゴリの過去のテキストを参照してみてください。
続いて、カメラを定義するためのコードが登場しています。カメラの位置、カメラの向き、そしてカメラの上方向をそれぞれ vec3
型のベクトルとして定義します。そして外積を用いて、カメラの横方向についても算出しておきます。※なぜ外積でベクトルが求められるのかここでは解説しませんが、このあたりの 3D プログラミングでよく登場する概念は絶対に勉強しておいたほうが得です!
コメントで、フォーカスする深度と書かれている部分がありますね。変数 targetDepth
に 0.1 を代入している箇所です。これは先々、レイマーチングについて理解が深まってくるとこの値の意味するところが自然とわかってくると思いますが、現時点では深く考えずに 0.1 を指定しておきましょう。
そしてその下。ここでいよいよレイを定義しています。先ほど定義したカメラの情報を使っているのがわかりますね。
今回のサンプルでは特別なことはせず、求められたレイベクトルをそのまま色として出力しています。よく見てみると、 gl_FragColor
に値を代入する部分でレイの Z 成分だけ正負を反転していることに気がつくと思います。これは、カメラの向きを定義している部分を見ればわかると思いますが、レイの Z 成分が負の方角へ向いているためそのまま出力しても真っ黒になって色がつかないためです。
このようなシェーダを走らせると、当テキストの冒頭にあったキャプチャ画像のような結果が得られます。
サンプルの実行結果
X は赤、Y は緑、そして Z が青という具合に、それぞれしっかり出力されているのが見て取れますね。
これでシェーダ内でのレイの算出ができるようになりましたので、次回以降、いよいよ本格的にレイマーチングのコードを記述していくことができるようになりました。
まとめ
さて、ちょっと数学的に難しい話題も出てきましたが、レイマーチングやレイトレーシング、そしてそれらのプログラムを書いていく上で欠かせないレイの定義について、理解できたでしょうか。
途中でも少し触れたように、レイマーチングは若干難易度が高く、基本的な 3D プログラミングの知識がないと苦しい場面が割とたくさん登場します。当テキストを読んでみて、これは厳しいかもしれないと感じられた方も、いらっしゃるかもしれません。
GLSL カテゴリでは、難易度がどうしても高くなってしまうことは正直覚悟するよりほかありません。しかし、その分理解できたときや、実際にサンプルが思い通りに動いたときの感動も大きなものになるでしょう。そして私自身がそうであったように、誰もが最初はわからない状態からスタートします。諦めず、しっかり基礎を勉強してから見返せば、きっとだんだんイメージできるようになっていくはずです。
実際問題、私自身も勉強しながらテキストを書いています。もしかしたら、間違ったことを書いてしまうこともあるかもしれませんが、それはきっと偉い人が教えてくれると思いますので、そうしたらまた勉強すればいいことです。焦らずじっくり、取り組んでみていただけたらと思います。
次回以降、実際にレイマーチングによるレンダリングに挑戦していきます。できる限りわかりやすく、できる限り詳細に解説できたらと思っていますので、一緒にがんばりましょう。
実際に動作するサンプルはいつものように以下のリンクから。