オブジェクトにテクスチャなどを投影する
今回のサンプルの実行結果
よりグラフィカルなシーンを
前回は、行列をうまく利用することで、レイマーチングでオブジェクトを捻じるように座標変換する方法について解説しました。
割と単純な造形になってしまいがちなレイマーチングのオブジェクトも、行列を活用して変形させると一気にその表情が変わります。見た目にも面白いレンダリング結果を得るためにも、使いどころは限定されるものの有用なテクニックになるのではないでしょうか。
さて今回ですが、行列からは一度離れて、よりグラフィックス処理のクオリティをアップさせるためのテクニックとしてテクスチャや二次元エフェクトの投影をレイマーチングで実現させてみましょう。
今回の内容をしっかり習得することができれば、レイマーチングにテクスチャを持ち込むことが可能になります。どうしても幾何学的な図形ばかりになってしまいがちなレイマーチング。テクスチャを利用できることの優位性はそれなりに大きいと思います。
それほど概念も難しくないので、ぜひがんばって挑戦してみてください。
通常の WebGL とは異なる概念
レイマーチングは、これまで当サイトのテキストを読み進めてきた方であればわかっていると思いますが、通常の WebGL のプログラムとはいろいろな面でかなり相違点があります。
普通、WebGL でテクスチャを利用するとなったら、モデルの頂点データを生成する段階でテクスチャの UV 座標を事前に準備しておき、javascript 側で VBO に加工してからシェーダにプッシュするのが一般的でしょう。
しかし、レイマーチングにはモデルの頂点データを受け取るという概念が基本的にはありませんね。もちろん絶対にそのような実装ができないということではありませんが、少なくとも今まで見てきたレイマーチングの実装にはそのような概念はありませんでした。オブジェクトの形状を決めるのは distance function であって、シェーダに送られてきたモデルデータではないわけですね。
となると、通常の WebGL プログラムと同じ要領で、テクスチャをシェーダ側で参照して利用できるように調整したとして、どうやってそのサンプリングされたデータをモデルに適用したらいいのでしょうか。UV 座標がそもそも存在しないのに、テクスチャを貼ることなんてできないんじゃないか? そんなふうに思ってしまう方もいらっしゃるかもしれませんね。
しかし、そこは要するに創意工夫です。
今までもそうだったように、UV 座標がプッシュされてこないなら、シェーダ内で計算してしまえばいいんです。
シェーダ内で動的に計算を行う上で、ポイントなるところはどこなのか。次項よりさらに詳しく見ていきましょう。
レイマーチングだからこそ得られる情報を利用
通常の WebGL と比較した場合、その実装方法の都合上、レイマーチングだからこそ得られる情報というのがいくつかあります。
その代表格がオブジェクトとレイの交点です。
WebGL で普通にオブジェクトをレンダリングする場合、そもそもレイという概念自体、持ち出す必要性がありませんね。しかし逆に、レイマーチングではレイとオブジェクトが衝突したかどうかがレンダリングの指標になるわけですから、レイや、レイとオブジェクトとの交点といった情報は必ず利用することになります。
要は、この交点の情報と distance function を活用することで、シェーダ内部で動的に UV を生成してやるわけです。
思い出してみてほしいのですが、レイマーチングにおいてはオブジェクトの法線でさえも、やはり同様にレイとオブジェクトとの交点の情報を用いて動的に生成していましたよね。UV 座標に関しても、これとまったく同じようなことをしないといけないのは考えてみれば当たり前ですね。
ちなみに、いわゆる普通の、オブジェクトの表面をぴったりと覆うような UV 座標を計算で割り出すのは、当然ながらオブジェクトの形状によってかなり難易度に差が出ます。比較的単純な形状である平面や球体は、UV の算出も割と簡単に行えます。
当然このあたりの処理は数学に精通しているほうが有利なのは言うまでもありませんが、今回はこの分野に深く足を突っ込んでいくのではなく、まずは簡単に実装することが可能なテクスチャなどをオブジェクトに投影する方法について考えてみましょう。
二次元データを投影する
当サイトのテキストの中には、レイマーチングではなく通常の WebGL のテクニックとして、テクスチャの投影を利用しているものがあります。
テクスチャの投影は射影テクスチャマッピングや、光学迷彩シェーダ、あるいはシャドウマッピングなどでも使われる比較的メジャーな手法です。
それらと比較した場合、レイマーチングはどちらかというとむしろ簡単に二次元データの投影ができます。これは、先ほども書いたようにレイとオブジェクトの交点という情報がシェーダ内で求められるからですね。
今回は、ちょっと長いですがフラグメントシェーダのコード全文を一度掲載してみます。
レイマーチング投影シェーダのコード
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
const vec3 cPos = vec3(0.0, 5.0, 5.0);
const vec3 cDir = vec3(0.0, -0.707, -0.707);
const vec3 cUp = vec3(0.0, 0.707, -0.707);
const vec3 lightDir = vec3(-0.57, 0.57, 0.57);
// torus distance function
float distFuncTorus(vec3 p){
p.xz -= mouse * 2.0 - 1.0;
vec2 t = vec2(3.0, 1.0);
vec2 r = vec2(length(p.xz) - t.x, p.y);
return length(r) - t.y;
}
// floor distance function
float distFuncFloor(vec3 p){
return dot(p, vec3(0.0, 1.0, 0.0)) + 1.0;
}
// distance function
float distFunc(vec3 p){
float d1 = distFuncTorus(p);
float d2 = distFuncFloor(p);
return min(d1, d2);
}
vec3 genNormal(vec3 p){
float d = 0.0001;
return normalize(vec3(
distFunc(p + vec3( d, 0.0, 0.0)) - distFunc(p + vec3( -d, 0.0, 0.0)),
distFunc(p + vec3(0.0, d, 0.0)) - distFunc(p + vec3(0.0, -d, 0.0)),
distFunc(p + vec3(0.0, 0.0, d)) - distFunc(p + vec3(0.0, 0.0, -d))
));
}
void main(void){
// fragment position
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / max(resolution.x, resolution.y);
// camera and ray
vec3 cSide = cross(cDir, cUp);
float targetDepth = 1.0;
vec3 ray = normalize(cSide * p.x + cUp * p.y + cDir * targetDepth);
// marching loop
float tmp, dist;
tmp = 0.0;
vec3 dPos = cPos;
for(int i = 0; i < 256; i++){
dist = distFunc(dPos);
if(dist < 0.001){break;}
tmp += dist;
dPos = cPos + tmp * ray;
}
// hit check
vec3 color;
if(abs(dist) < 0.001){
// generate normal
vec3 normal = genNormal(dPos);
float diff = clamp(dot(lightDir, normal), 0.1, 1.0);
// generate tile pattern
float u = 1.0 - floor(mod(dPos.x, 2.0));
float v = 1.0 - floor(mod(dPos.z, 2.0));
if((u == 1.0 && v < 1.0) || (u < 1.0 && v == 1.0)){
diff *= 0.7;
}
color = vec3(1.0, 1.0, 1.0) * diff;
}else{
color = vec3(0.0);
}
gl_FragColor = vec4(color, 1.0);
}
いやー、長いですね(笑)
ちょっと面喰ってしまうかもしれませんが、落ち着いて、ポイントを絞って考えてみてください。
レイマーチングのマーチングループ部分をまずは抜粋して中身を見てみましょう。
マーチングループ部分を抜粋
// marching loop
float tmp, dist;
tmp = 0.0;
vec3 dPos = cPos;
for(int i = 0; i < 256; i++){
dist = distFunc(dPos);
if(dist < 0.001){break;}
tmp += dist;
dPos = cPos + tmp * ray;
}
ここでポイントになるのは、for
文による繰り返し処理の中で distance function の戻り値をチェックしている点です。
もし、distance function の戻り値が一定以下(今回の場合は 0.001 以下)だった場合にはループを抜けるように書かれているのがわかると思います。もし、レイとオブジェクトが衝突している場合には distance function の戻り値は非常に小さいものになります。これで衝突判定をしているのでしたね。
distance function の戻り値が非常に小さく、レイとオブジェクトが衝突していると判定されたとき、レイとオブジェクトの交点の座標はそのとき distance function に渡されていた座標のはずです。つまり、上記でいうと変数 dPos
の中身を見れば、レイのその時点での先端の座標=レイとオブジェクトの交点がわかるはずですね。
そしてさらにその下のコードで、法線の算出を行っている部分があります。
そこに、今回追加されたコードがあります。このコードが、タイル模様を生成するためのコードです。ここも抜粋して再度掲載してみましょう。
タイル模様を生成するコード
// generate normal
vec3 normal = genNormal(dPos);
float diff = clamp(dot(lightDir, normal), 0.1, 1.0);
// generate tile pattern
float u = 1.0 - floor(mod(dPos.x, 2.0));
float v = 1.0 - floor(mod(dPos.z, 2.0));
if((u == 1.0 && v < 1.0) || (u < 1.0 && v == 1.0)){
diff *= 0.7;
}
コメントで generate tile pattern
と書かれているところが該当するコードです。これを見ると、変数 u
と、変数 v
に何かしらの計算結果を代入しています。
先ほども書いたように、変数 dPos
にはレイとオブジェクトが衝突した交点の座標が入っています。その X 値と Z 値とを利用して mod
を使った計算をしています。
ここは交点の座標を mod
関数に通すことで 0.0 ~ 1.99999... の範囲内に変換し、さらにそれを floor
に通すことで 0.0 か 1.0 のいずれかの数値を算出させるようになっています。
0.0 か、もしくは 1.0 に値が定まったら、その値を使って 1.0 から減算処理をしてやります。これを行うと、交点の座標がどういった座標であっても変数 uv
には 0.0 か、もしくは 1.0 のいずれかが二者択一で入ることになります。
二者択一までくれば、あとはもう条件式でタイル模様になるように振り分けをしてやるだけですね。上記の抜粋コードで言うと diff
という変数には法線でライティングを計算した際の係数が入っているので、これに対して特定の条件を満たしている場合だけ 0.7 を掛けて若干暗くなるようにしてやります。
これで、XZ 平面上にタイル模様が投影されます。
まとめ
先ほどの例では、シェーダ内でタイル模様を動的に生成して、それをモデルに投影しました。
しかし、外部から読み込んだ画像データなどをテクスチャとしてシェーダに送り、それを利用してマッピングしたい場合にはどうしたらいいのでしょうか。
でもこれはもう冷静に考えればわかりますね。先ほどのコードをほんの少し修正すれば、投影用の UV 座標に適した数値を計算するのは簡単だと思います。mod
を使って座標を調整して値を 0.0 ~ 1.0 に修正してやればいいんですね。
もちろん、値の調整方法やテクスチャパラメータを工夫すれば、シームレスにテクスチャを貼りつけたりすることも簡単です。
また、今回は XZ 平面に向かって二次元データを投影しましたが、たとえば一般的なプロジェクターのように垂直な壁に向かって投影するようなケースでは、計算を XY 座標で行うようにすればいいだけです。これは直感的にイメージしやすいと思います。
単純に見た目を変更するために投影する以外にも、たとえば投影座標のテクスチャの値を持ってきてさらに計算を行ったりといったことも可能です。これを工夫すると、ノイズのテクスチャから画像を読みだして凹凸をつけたりといった一風変わった処理にも応用が利きます。
使い方も含めて、いろいろ考えてみると楽しいと思います。
実際に動作するサンプルはいつものように以下のリンクから。