レイマーチングで屈折表現

空いていたので、[WebGL Advent Calendar 2018](https://qiita.com/advent-calendar/2018/webgl) 5日目の記事にしました(WebGLというかGLSLですが...）。

---

12月1,2日に行われた[Tokyo Demo Fest 2018](http://tokyodemofest.jp/2018/)のGLSL Compoに「Refaction」という作品を提出しました。
実際に動いているものは以下から見ることができます（少し重いので注意してください）。
http://glslsandbox.com/e#50703.0

![screenshot02_small.png](https://qiita-image-store.s3.amazonaws.com/0/61569/a6174ee7-8398-7b49-b8a1-13f81b4a3961.png)

この作品では、真ん中のガラスっぽい直方体をレイマーチングで作成しています。
ここで使用している屈折表現について解説します。

---

まず、カメラから通常のレイマーチングと同じようにオブジェクトの表面に到達するまでレイを飛ばします。表面に到達したら、GLSLの組み込み関数[refect](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/reflect.xhtml)と[refract](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/refract.xhtml)を使って反射方向と屈折方向を求めます。

![fig1.png](https://qiita-image-store.s3.amazonaws.com/0/61569/178042f7-1341-47e2-3a00-cb5a1c3f7c42.png)

反射方向はスペキュラー成分を求めるために使用し、屈折方向は背景をサンプリングすることに用います。

GLSLで実装すると以下のようになります。

```glsl

float schlickFresnel(float ri, float cosine) {
	float r0 = (1.0 - ri) / (1.0 + ri);
	r0 = r0 * r0;
	return r0 + (1.0 - r0) * pow(1.0 - cosine, 5.0);
}

float refractionIndex = 1.5;
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
vec3 lightColor = vec3(2.0);
vec3 substanceColor = vec3(0.90, 0.95, 1.0);

vec3 raymarch(vec3 ro, vec3 rd) {
	vec3 p = ro;
	for (int i = 0; i < 32; i++) {
		float d  = scene(p);
		p += rd * d;
		if (d < 0.01) {
			vec3 n = normal(p);
			vec3 reflected = reflect(rd, n);
			float f = schlickFresnel(refractionIndex, max(0.0, dot(-rd, n)));
			vec3 spec = f * lightColor * pow(max(0.0, dot(reflected, lightDir)), 4.0);
			vec3 refracted = refract(rd, n, 1.0 / refractionIndex);
			return spec + substanceColor * background(p, refracted);
		}
	}
	return background(ro, rd);
}
```

スペキュラー成分はPhongシェーディングで計算しています。Schlickの近似式で求めたフレネル係数をかけることで、浅い角度から見たときに光がより反射するようになります。

オブジェクトと直方体を描画するとそれぞれ次のようになります。

<img width="50%" alt="one-refraction-sphere.png" src="https://qiita-image-store.s3.amazonaws.com/0/61569/d7377493-bc02-a60b-4204-45744996911f.png">

<img width="50%" alt="one-refraction-box.png" src="https://qiita-image-store.s3.amazonaws.com/0/61569/3193efb9-ba31-26ae-c9b1-5fcb4c754a60.png">

---

ここまででもそれなりに雰囲気がでますが、レイの射出時の再屈折をさらに考慮してみたいと思います。

再屈折を考慮する場合には、屈折したレイが背面のどこから射出するかを知る必要があります。これは、屈折したレイの先にある十分に遠くの地点からそのレイの反対向きにレイを飛ばすことで実現します。

![fig2.png](https://qiita-image-store.s3.amazonaws.com/0/61569/6cbfcfaf-b187-bac9-84cf-fe657c26f66d.png)



再屈折する位置がわかったので、ここでもう一度`refract`関数を用いて射出方向を計算します。射出方向が求まれば、そこから背景をサンプリングできます。注意しないといけないのは、入射角がある値を超えると全反射が起きて屈折しないことがある点です。その場合には`refract`関数は`vec3(0.0)`を返します。ここからレイがオブジェクトから射出するまで同じ処理を繰り返してもいいのですが、パフォーマンス的なことも考えて今回は反射方向で背景をサンプルすることにします。

![fig3.png](https://qiita-image-store.s3.amazonaws.com/0/61569/4066e93d-b89f-0aff-5ebd-f5bd7a0e9ad4.png)

これをGLSLで実装すると以下のようになり、2回レイマーチングを行うようになります。

```glsl
vec3 raymarch(vec3 ro, vec3 rd) {
	vec3 p = ro;
	for (int i = 0; i < 32; i++) {
		float d  = scene(p);
		p += rd * d;
		if (d < 0.01) {
			vec3 n = normal(p);
			vec3 reflected = reflect(rd, n);
			float f = schlickFresnel(refractionIndex, max(0.0, dot(-rd, n)));
			vec3 spec = f * lightColor * pow(max(0.0, dot(reflected, lightDir)), 4.0);
			vec3 refracted = refract(rd, n, 1.0 / refractionIndex);			
			vec3 op = p + refracted * 100.0;
			for (int j = 0; j < 32; j++) {
				float d = scene(op);
				op += -refracted * d;
				if (d < 0.01) {
					vec3 n2 = normal(op);
					vec3 refracted2 = refract(refracted, -n2, refractionIndex);
					if (length(refracted2) > 0.01) {
						return spec + substanceColor * background(op, refracted2);
					} else {
						vec3 reflected2 = reflect(refracted, -n2);
						return spec + substanceColor * background(op, reflected2);
					}
				}
			}
		}
	}
	return background(ro, rd);
}
```

このようにしたときの球と直方体は以下のようになります。入射位置での屈折方向で背景をサンプリングしたときと比べると、特に直方体での背面の角の部分がリアルになっている感じがします。

<img width="50%" alt="multi-refraction-sphere.png" src="https://qiita-image-store.s3.amazonaws.com/0/61569/bcb40d9d-11ba-0a78-ed97-dbb755816667.png">

<img width="50%" alt="multi-refraction-box.png" src="https://qiita-image-store.s3.amazonaws.com/0/61569/50d95127-19bd-d00c-e09b-e2ae9c283ce3.png">



なお、この方法は凸面体ならば（たぶん）上手くいきますが、凹面体では上手くいかないです。
例えばトーラスで同じアルゴリズムを適用すると以下のようになり、正確に再現できていません。

![fig4.png](https://qiita-image-store.s3.amazonaws.com/0/61569/941ebcaa-f587-64bc-1004-500e5cf672c3.png)

<img width="50%" alt="multi-refraciton-torus.png" src="https://qiita-image-store.s3.amazonaws.com/0/61569/d089d811-3807-6645-9e68-6d653af766fd.png">

---

今回のサンプルは以下から確認できます。`scene`関数から描画するオブジェクトの形状を変更できます。
また、`#define MULTI_REFRACTION`をコメントアウトすることで入射位置での屈折方向で背景をサンプリングするようになります。
http://glslsandbox.com/e#50748.0