レイトレで背景(スカイスフィア)を作る方法

背景の必要性

金属の質感は周りの背景が映りこまないとそれっぽくならない。コーネルボックスとかだと壁などがあるため、その他の物体が移りこむことによって金属らしさが出てくる。

特に金属のBSDFの比較などで幾つも球体が置いてある画像がよくあるが、大体はコーネルボックスとかに入れていなくて、外みたいな背景が映っていることが多いでしょう。BSDFを作る際にはこうした背景があったほうがよりバグがわかりやすいので、BSDFのテストの時は背景を作っておいた方が良いでしょう。ということで、背景の作り方についてこの記事で私が作った方法で紹介しようと思います。(もっといい方法はあると思います)

スカイスフィアとは

3Dにおける背景は実は図1のように結構単純に作られている。全体を覆うような大きな球を作り、その内側にテクスチャをぺたっと張り付けるような形で作っている(Unityとかだと立方体だったりする)。この球はスカイスフィアとか天球などと呼ばれたりします。

処理の内容としては

  1. どのポリゴンにも衝突しなかった
  2. スカイスフィアとの衝突判定を行う
  3. 衝突位置を球面座標に変換する
  4. 球面座標の角度$ \theta $と$ \phi $をUVとして扱う
  5. HDR画像から輝度を取得する といったような形で行います。

球面座標への変換とUV

巨大な球を用意して、どのポリゴンとも衝突しなかったときにその球との衝突判定を行い、球の衝突座標を求めます。 スカイスフィアのUVはこの衝突座標の緯度、経度を使うため、その衝突座標$(x,y,z)$を球面座標$(r,\theta,\phi)$へと変換する必要があります。

直交座標から球面座標の変換は以下のようなしきで行える。ただしこの式は3Dでよく使われる右手系座標でのものです(左手系なら単にyとzを切り替えれば大丈夫)。 $$ r = \sqrt{x^2 + y^2 + z^2} $$ $$ \theta = \arccos{y/r} $$ $$ \phi = \mathrm{sgn}(y) \arccos(x/\sqrt{x^2 + z^2}) $$ $\mathrm{sgn}$は符号関数で$y$が負なら-1を、正なら1を返す関数です、単にif文で実装してもいいと思います。また、$r$はUVの計算には使わないので事前に$(x,y,z)$を正規化しておけば$r=1$になるので計算が楽になります。

以上の計算によって得られた$\theta$及び$\phi$をuvとして使いたいため、0 ~ 1に正規化します。 それぞれ$ 0 \leq \theta \leq \pi $、$ -\pi \leq \phi \leq \pi$であるため、 $$ u = \frac{\theta}{\pi} $$ $$ v = \frac{\phi + \pi}{2\pi} $$ という式でそれぞれUVが求められます。

HDR画像

スカイスフィアに通常の.pngなどのテクスチャを貼ると実際には暗くなってしまう。これは画像の値が1.0以上を取ることがないためである。

現実では輝度というのは上限がない。例えば太陽の輝度というのは1.0をはるかに超えるほどの明るさであり、例えpngなどで太陽がある風景を背景テクスチャとして使っても、太陽の明るさは1.0へと落とされ相当暗くなり正しい見た目にはならない。(太陽だけでなく普通の風景とかでも輝度は1.0を越える。)

こうした、輝度の情報をそのまま保存する形式が必要ということで作られた画像形式がある。それがHDR画像と呼ばれるものである。

これは輝度の数値をそのまま保存してるもので1.0より高い数値が保存されている。基本的にどの3Dソフトでも背景テクスチャというのはこのHDR画像を用いる。HDR画像は簡単に得ることができて、「free HDRI」などと検索するとフリーのHDR画像を置いてあるサイトが結構出てくるのでお好きなのを取ってくるとよいでしょう。

HDRIテクスチャサイト HDRI Haven

https://hdrihaven.com/

このHDR画像を先ほどのUVからスカイスフィアに張り付けることで背景というのは簡単に作れる。(HDRIの読み込みの方が大変な気がする)

コード例

UVの計算の一例として載せておきます。(vec3やvec2などは勝手に決めたものです)

vec2 WorldSphereUV(Ray r){

	//衝突情報を入れるもの、今回は衝突座標だけでよい
	Intersectinfo Info;
	
	//半径10000の球の衝突判定
	Spherehit(r,info,10000);
	
	vec3 pos = normalize(info.position);
	float x = pos[0];
	float y = pos[1];
	float z = pos[2];

	//極座標の角度を計算
	float theta = std::acos(y);
	float phi = std::acos(x / std::sqrt(x * x + z * z));
	if(y < 0) phi *= -1;
	//UVの計算	
	float u = theta / Pi;
	float v = (phi + Pi) / (2 * Pi);

	return vec2(u,v);
}

もし、画像が反転してたら

 v = 1.0 - v;

とすれば治るかと思われます。

読み込んだ例