放物面鏡への映り込みを環境のテクスチャに使う
環境マッピングの手法には、スフィアマッピングとキューブマッピングのほかに、放物面マッピングと呼ばれる手法があります。この方法は Heidrich と Seidel によって提案されました1。放物面というのは、パラボラアンテナに使われる曲面 (paraboloid) です。この方法はスフィアマッピングやキューブマッピングのように OpenGL に専用の機能が用意されているわけではありませんが、マルチテクスチャを使えば簡単に実装できます。
放物面マッピングの特徴
スフィアマッピングは環境のテクスチャを簡単に作成できますが、実写だと球状の鏡を撮影する際にカメラが映り込んでしまいますし、視点の位置を自由に変えることもできません。一方、キューブマッピングでは視点の位置を任意に設定できますが、画像が6枚必要になります。これらの画像は平面的なのでコンピュータでレンダリングして作るのは楽ですが、実写だと撮影や画像の接合に手間がかかったりします。
これに対して放物面マッピングでは、環境のテクスチャを放物面鏡、あるいは近似的に魚眼レンズを使って作成することができます。魚眼レンズがその辺に転がっているとは思えませんが、これを使えばスフィアマッピングのようにカメラが映り込むことはありません。またこの方法は2枚の画像だけでキューブマッピングと同様に視点を任意の位置に設定することができます。
放物面鏡の性質
放物面鏡の凹面鏡には、入射した平行光線が1点(焦点)に集まるという性質を持っています。これに対して、放物面鏡の凸面鏡(凹面鏡の裏側)に光を当てると、反射光は焦点と反射点を結んだ直線の方向に反射します。
| 放物面鏡の内側に平行光線を当てた場合 | 放物面鏡の外側に平行光線を当てた場合 |
|---|---|
![]() |
![]() |
そこで、この放物面鏡を焦点の位置で切断し、2つを切断面で貼り合わせた鏡を考えます。そうすると、2つの放物面鏡の映り込みを組み合わせて、全方位の環境を保持できることがわかります。
| 放物面鏡の凸側から見たとき | 放物面鏡の凹側から見たとき |
|---|---|
![]() |
![]() |
したがって、反射ベクトルの z 成分が正の場合は表の鏡に映った画像をテクスチャに用い、z 成分が負の場合は裏の鏡に映った画像をテクスチャに用いれば、環境全体をくまなくマッピングできることになります。
テクスチャ座標の算出
それでは、反射ベクトル $(r_x, r_y, r_z)$ からテクスチャ座標 $(s, t)$ を算出してみましょう。今、$x^2 + y^2 = z$ という回転放物面の焦点が原点となるように平行移動して、$x^2 + y^2 = z + 0.25$ という曲面について考えてみます。

という曲面は回転放物面なので、
\[x^2+y^2=r^2\]とおくと、
\[r^2 = z + 0.25\] \[z = r^2 - 0.25\]となります。したがって、反射ベクトルの $z$ に対する角度 $\theta$ は、
\[\tan\theta = \frac{r}{z} = \frac{r}{r^2 - 0.25}\] \[r^2\tan\theta - r - 0.25\tan\theta = 0\]ですから、これを $r$ について解くと、
\[\begin{aligned} r &= \frac{1+\sqrt{1+\tan^2\theta}}{2\tan\theta} = \frac{1+\sqrt{1/\cos^2\theta}}{2\tan\theta} \\ &= \frac{\cos\theta}{2\sin\theta}\left(1+\frac{1}{\cos\theta}\right) = \frac{\cos\theta+1}{2\sin\theta} \end{aligned}\]となります。ここで、
\[\cos\theta = r_z\] \[\sin\theta = \sqrt{r_x^2+r_y^2} = \sqrt{1-r_z^2}\]ですから、
\[\begin{aligned} s &= \frac{r_x}{\sqrt{r_x^2+r_y^2}}r = \frac{r_x}{\sqrt{r_x^2+r_y^2}}\frac{\cos\theta+1}{2\sin\theta} \\ &= \frac{r_x}{\sqrt{r_x^2+r_y^2}}\frac{r_z+1}{2\sqrt{r_x^2+r_y^2}} = \frac{r_x(r_z+1)}{2(1-r_z^2)} \\ &= \frac{r_x(r_z+1)}{2(1-r_z)(1+r_z)} = \frac{r_x}{2(1-r_z)} \end{aligned}\]となります。同様にして、
\[t = \frac{r_y}{2(1-r_z)}\]が得られます。
これは裏側の鏡に映った画像に対するテクスチャ座標です。表側の鏡に対するテクスチャ座標は、単に $r_z$ の符号を反転するだけで求めることができます。また、このままでは $(s, t)$ は $[-0.5, 0.5]$ の範囲になりますから、$[0, 1]$ のテクスチャ座標の範囲に収まるよう、$s$, $t$ のそれぞれに $0.5$ を足しておきます。
$r_z<0$ のときは、次のテクスチャ座標で裏面のテクスチャを標本化します。
\[s = \frac{r_x}{2(1-r_z)} + 0.5\] \[t = \frac{r_y}{2(1-r_z)} + 0.5\]これを行列で表します。
\[\begin{pmatrix} s' \\ t' \\ 0 \\ q' \end{pmatrix} = \begin{pmatrix} 1 & 0 & -1 & 1 \\ 0 & 1 & -1 & 1 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & -2 & 2 \end{pmatrix} \begin{pmatrix} r_x \\ r_y \\ r_z \\ 1 \end{pmatrix}\]$r_z\geq 0$ のときは、次のテクスチャ座標で表面のテクスチャを標本化します。
\[s = \frac{r_x}{2(1+r_z)} + 0.5\] \[t = \frac{r_y}{2(1+r_z)} + 0.5\]これを行列で表します。
\[\begin{pmatrix} s' \\ t' \\ 0 \\ q' \end{pmatrix} = \begin{pmatrix} 1 & 0 & 1 & 1 \\ 0 & 1 & 1 & 1 \\ 0 & 0 & 0 & 0 \\ 0 & 0 & 2 & 2 \end{pmatrix} \begin{pmatrix} r_x \\ r_y \\ r_z \\ 1 \end{pmatrix}\]放物面マッピングの実装
上の式より、キューブマッピングと同様にテクスチャ座標として反射ベクトルを用い、テクスチャ変換行列に上記の行列を設定しておけば、放物面マッピングが実現できることになります。本当はキューブマッピングと違って、この手法ではテクスチャ座標の第4要素 $q$ が必ず $1$ である必要があります。しかし、テクスチャ座標のデフォルト値は $(0, 0, 0, 1)$ なので、ここで $q$ を自動生成しなければ、$q$ は多分 $1$ のままだろうという甘い考え方を採用します。
もう一つ問題があります。この手法では二つのテクスチャを使い分けまが、どちらのテクスチャを使うのかは画素単位に判断しなければなりません。この判断は反射ベクトル r_z の符号を見て行うことができるのですが、OpenGL には(プログラマブルシェーダを使わなければ)このような判断を組み込む余地がありません。
しかし、$(s, t)$ が一方のテクスチャの範囲内にあれば、もう一方のテクスチャでは $(s, t)$ は必ずテクスチャの範囲外になるはずです。そこで Heidrich と Seidel は、アルファテストを使って範囲外のポリゴンを削り取り、表側と裏側に分けて2回描くという手法を採用しています。
一方 Real-Time Rendering の本には、双方のテクスチャの範囲外の色を黒にして、2つのテクスチャを単に加算するという方法が示されています。確かに、こうすれば範囲内にあるほうのテクスチャの色がマッピングされるはずです。
ということで、こっちの方法を使って実際にやってみましょう。雛形のプログラムにはマルチテクスチャのときに使ったものを流用します。
準備
マルチテクスチャを使って実装するので、Windows ではまず glext.h の読み込みと、関数ポインタ変数 glActiveTexture の宣言を行っておいてください。
#if defined(__APPLE__)
# define GL_SILENCE_DEPRECATION
# include <GLUT/glut.h>
# include <OpenGL/glext.h>
#else
# if defined(_MSC_VER)
//# pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
# define _USE_MATH_DEFINES
# define _CRT_SECURE_NO_WARNINGS
# endif
# include <GL/glut.h>
# include <GL/glext.h>
# if defined(_WIN32)
PFNGLACTIVETEXTUREPROC glActiveTexture;
# endif
#endif
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
あらかじめテクスチャの境界色に使う変数 border を宣言し、それに黒色を設定しておきます。この色は、テクスチャのラッピングモード (GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T など) に GL_CLAMP_TO_BORDER や GL_CLAMP_TO_BORDER を指定したときに、テクスチャの範囲外をサンプリングしたときの色になります。この色のアルファ値 A を 0 にしておけば、テクスチャ環境に GL_DECAL を指定することによって、テクスチャの範囲外に下地の色やテクスチャを表示することができます。この理由は後述します。
/*
** 初期化
*/
static void init()
{
/* テクスチャ画像はワード単位に詰め込まれている */
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
/* テクスチャの読み込みに使う配列 */
GLubyte texture[TEXHEIGHT * TEXWIDTH * 4];
/* テクスチャ画像の読み込み */
FILE* fp = fopen(texture_file, "rb");
if (fp != NULL) {
fread(texture, sizeof texture, 1, fp);
fclose(fp);
}
else {
perror(texture_file);
}
/* テクスチャの境界色 */
static const GLfloat border[] = { 0.0f, 0.0f, 0.0f, 0.0f };
また今回は、もともとあった下地のテクスチャと合わせて合計3つのテクスチャを使うので、テクスチャオブジェクトを3つ作成しておきます。さらに Windows の場合は、関数ポインタ変数 glActiveTexture に glActiveTexture() の実体のエントリポイントを代入しておきます。
#if defined(_WIN32)
glActiveTexture =
(PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture");
#endif
/* テクスチャ名を3つ作る */
GLuint texname[3];
glGenTextures(3, texname);
テクスチャの割り当て
まず、下地のテクスチャをマッピングします。マルチテクスチャを使うので、このテクスチャはテクスチャユニット0 (GL_TEXTURE0) に割り当てます。実は、このテクスチャは、この上に重ねる映り込みのテクスチャに隠されて表示されないのですが、後で使うつもりなので、今はとりあえずこうしておきます。
/* 1つ目のテクスチャユニットには下地のテクスチャを割り当てる */
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texname[0]);
/* テクスチャの割り当て */
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
GL_RGBA, GL_UNSIGNED_BYTE, texture);
/* テクスチャを拡大・縮小する方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
/* テクスチャの繰り返し方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
/* 下地のテクスチャのテクスチャ環境 */
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
次に、裏面の放物面テクスチャをマッピングします。このテクスチャマッピングにはテクスチャユニット1を使います。放物面テクスチャは2次元テクスチャ (GL_TEXTURE_2D) としてマッピングします。
/* 2つ目のテクスチャユニットには裏面の放物面テクスチャを割り当てる */
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texname[1]);
/* テクスチャ画像の読み込み */
if ((fp = fopen("paraboloid1.raw", "rb")) != NULL) {
fread(texture, sizeof texture, 1, fp);
fclose(fp);
}
/* テクスチャの割り当て */
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
GL_RGBA, GL_UNSIGNED_BYTE, texture);
GL_TEXTURE_WRAP_S と GL_TEXTURE_WRAP_T に GL_CLAMP_TO_BORDER を設定して、境界色がテクスチャの周囲に拡張されるようにします。そしてテクスチャの境界色 GL_TEXTURE_BORDER_COLOR に黒色 (border) を設定すれば、テクスチャからはみ出た部分が黒になります。あと、ここでは裏側の放物面テクスチャで下地のテクスチャを GL_REPLACE で置き換えてしまいます。
/* テクスチャを拡大・縮小する方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
/* テクスチャの繰り返し方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
/* テクスチャの境界色を黒にする */
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
/* 裏面のテクスチャのテクスチャ環境 */
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
テクスチャ座標として、反射ベクトルを自動生成するようにします。また、このテクスチャ座標の変換行列に、前述の行列を設定しておきます。このあたりが、この手法の一番のミソですね。
/* 反射ベクトルをテクスチャ座標として使う */
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
/* 裏面のマッピングに使うテクスチャ変換行列の設定 */
glMatrixMode(GL_TEXTURE);
static const GLdouble mat1[] = {
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
-1.0, -1.0, 0.0, -2.0,
1.0, 1.0, 0.0, 2.0,
};
glLoadMatrixd(mat1);
glMatrixMode(GL_MODELVIEW);
表面のテクスチャについても、同様の設定を行います。このテクスチャマッピングにはテクスチャユニット2を使います。
/* 3つ目のテクスチャユニットには表面の放物面テクスチャを割り当てる */
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, texname[2]);
/* テクスチャ画像の読み込み */
if ((fp = fopen("paraboloid2.raw", "rb")) != NULL) {
fread(texture, sizeof texture, 1, fp);
fclose(fp);
}
/* テクスチャの割り当て */
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0,
GL_RGBA, GL_UNSIGNED_BYTE, texture);
これも GL_TEXTURE_WRAP_S と GL_TEXTURE_WRAP_T に GL_CLAMP_TO_BORDER を設定して、境界色がテクスチャの周囲に拡張されるようにします。ただし、表面のテクスチャのテクスチャ環境は GL_ADD にして、表面のテクスチャを裏面のテクスチャに加算するようにします。このテクスチャ(表面)も下のテクスチャ(裏面)も範囲外を黒にしているので、これらが重なる部分が無ければ、これらを足すだけで裏と表を合成(接合)できるはずです。
/* テクスチャを拡大・縮小する方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
/* テクスチャの繰り返し方法の指定 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
/* テクスチャの境界色を黒にする */
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
/* 表面のテクスチャののテクスチャ環境 */
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD);
これもテクスチャ座標として、反射ベクトルを自動生成するようにします。また、このテクスチャ座標の変換行列にも、前述の行列を設定しておきます。
/* 反射ベクトルをテクスチャ座標として使う */
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_REFLECTION_MAP);
/* 表面のマッピングに使うテクスチャ変換行列の設定 */
glMatrixMode(GL_TEXTURE);
static const GLdouble mat2[] = {
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
1.0, 1.0, 0.0, 2.0,
1.0, 1.0, 0.0, 2.0,
};
glLoadMatrixd(mat2);
glMatrixMode(GL_MODELVIEW);
テクスチャの設定は以上です。残りは OpenGL の設定です。GL_CULL_FACE は glEnable() にして有効にしてもいいと思います。光源の設定は下地のテクスチャを隠してしまうので意味が無いんですけども。
/* 初期設定 */
glClearColor(0.3f, 0.3f, 1.0f, 0.0f);
glEnable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
/* 光源の初期設定 */
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glLightfv(GL_LIGHT0, GL_DIFFUSE, lightcol);
glLightfv(GL_LIGHT0, GL_SPECULAR, lightcol);
glLightfv(GL_LIGHT0, GL_AMBIENT, lightamb);
}
描画
シーンを描画する際は、二つの放物面テクスチャのマッピングとテクスチャ座標の自動生成を有効にします。描画が終わったら、それぞれを無効にします。
/*
** シーンの描画
*/
static void scene()
{
static const GLfloat color[] = { 1.0f, 1.0f, 1.0f, 1.0f }; /* 材質 (色) */
/* 材質の設定 */
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color);
/* 下地のテクスチャマッピング開始 */
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D);
/* 裏面のテクスチャマッピング開始 */
glActiveTexture(GL_TEXTURE1);
glEnable(GL_TEXTURE_2D);
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
/* 表面のテクスチャマッピング開始 */
glActiveTexture(GL_TEXTURE2);
glEnable(GL_TEXTURE_2D);
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
/* トラックボール処理による回転 */
glMultMatrixd(trackballRotation());
/* 箱を描く */
box(1.0, 1.0, 1.0);
/* 表面のテクスチャマッピング終了 */
glActiveTexture(GL_TEXTURE2);
glDisable(GL_TEXTURE_GEN_S);
glDisable(GL_TEXTURE_GEN_T);
glDisable(GL_TEXTURE_GEN_R);
glDisable(GL_TEXTURE_2D);
/* 裏面のテクスチャマッピング終了 */
glActiveTexture(GL_TEXTURE1);
glDisable(GL_TEXTURE_GEN_S);
glDisable(GL_TEXTURE_GEN_T);
glDisable(GL_TEXTURE_GEN_R);
glDisable(GL_TEXTURE_2D);
/* 下地のテクスチャマッピング終了 */
glActiveTexture(GL_TEXTURE0);
glDisable(GL_TEXTURE_2D);
}
これでプログラムの方は完成です。あとは使用する放物面テクスチャを用意するだけです。ここでは魚眼レンズを使って作成した、次の画像を使用してください。
使用した魚眼レンズは画像の中心からの距離と角度が比例しているもので、放物面鏡とは角度分布が異なります。ただ、放物面鏡の角度分布を調べてみると次のようなグラフになったので、「この程度なら人間の目はごまかされるやろ、環境マッピングやし」ということで、補正することなしにそのまま使っています。

プログラムの実行結果
これらのテクスチャを使ってプログラムを実際に動かしてみると、次のような実行結果が得られます。右の図は関数 box() を glutSolidSphere() に置き換えたものです。

箱にマッピングした場合は、真ん中に「お化け」のような妙なものが写っています。これは裏側のテクスチャが表側の領域に現れているようです。
表側のテクスチャの領域は裏側のテクスチャの領域の範囲外なので、本当ならこんなところに裏側のテクスチャが現れるはずはありません。しかし、裏側のテクスチャ座標を求める式の分母は $1-r_z$ となっており、$r_z = 1$ のとき、すなわち反射方向が表側の正面のときは、テクスチャ座標を求めることができません。これがきっとこの「お化け」の正体なんでしょう。
一方、球にマッピングしたときは、明るい円のようなものが見えています。これは裏面のテクスチャと表面のテクスチャが重なっている部分で、明度が加算されて明るくなってしまっているようです。これは表と裏のテクスチャの周囲が正確に一致するようテクスチャを丁寧に切り抜けば、目立たなくすることができます。でも、2枚のテクスチャの周囲を一致させる作業は、今回は手作業でやっているので、どうしても完全にはできませんでした。
そこで、GL_ADD を使うのをあきらめて、GL_DECAL を使うことにします。GL_DECAL なら、アルファ値を使って必要なところだけ貼り付けることができます。加算をしないので、裏側のテクスチャがにじみ出てきたり、周囲が重なって明るくなったりすることはありません。ただ、逆に表側のテクスチャが裏側 ($r_z = -1$) の面に現れる可能性がありますが、物体が閉じていれば裏側の面は見えないので、問題にはならないでしょう。
/* 表面のテクスチャののテクスチャ環境 */
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

球はまだ少しテクスチャの境界が見えていますが、だいぶましになりました。実はテクスチャ自体にも少し工夫してあります。使用した魚眼レンズは画角が 180°より少しだけ大きいらしく、周囲に若干の余裕がありました。そこで、このレンズで撮影した円周魚眼画像を画角 180°の所で正方形にクロップする際に、その画像の上下左右以外の 180°を超える部分のアルファ値 A を、テクスチャの内部のアルファ値 1 から外部の 0 に向かって線形にグラデーションを付けています。


この方法では環境の2枚のテクスチャを、下地の色が透けないように不透明にしてマッピングする必要があります。したがって、他のテクスチャと合成する場合には、これらの環境のテクスチャを最下層(テクスチャユニット0と1)に置いて、その上から他のテクスチャを合成する必要があります。
-
Heidrich, Wolfgang, and Hans-Peter Seidel. “View-independent environment maps.” Proceedings of the ACM SIGGRAPH/EUROGRAPHICS workshop on Graphics hardware. 1998. ↩





