PIXAR の映画
WALL·E 見てきました。私が今まで見てきた中で、一番きれいな CG を見せてくれた映画ではないかと思います(リアルだとか色彩がどうとかという話ではなく、「CG 的に」きれいだと思いました)。ゴミだらけでしたけど。いやそれにしても、あんなに大量のゴミをよく描いたなと思います。それと WALL·E とても可愛かったです。Mac ユーザは特にそう思うんじゃないでしょうか(私はそのシーンで吹きました)。
分光現象
前回のサンプルでは、環境のテクスチャが暗くて映り込みが見えにくかったので、テクスチャを作り直しました(マウスの左ボタンでオブジェクト、右ボタンで背景を回転できるようになっています)。
外側の箱のテクスチャの境界が目立たないように工夫して作ったつもりなんですが、やっぱり境界が見えますね。どうしてなのかなぁ。

屈折マッピングは視線を屈折させてテクスチャをサンプリングするだけの処理ですが、これで簡単にガラスのような質感を表現できます。しかし、屈折率は波長の関数なので(プリズムに当てた白色光が七色に分かれるのはこのため)、屈折光には色が付く(というか、色ずれが生じる)場合があります。これを分光現象とかフリンジ (fringe) とか色収差とか呼んでいます。
オレンジブックでは、これを非常に簡単に実現する方法が紹介されています。ここではこの方法について解説したいと思います(解説するまでもない気がしますが)。
この方法では、屈折方向を光の3原色 RGB の3つの成分のそれぞれについて求めます。このサンプルのバーテックスシェーダプログラム aberration.vert は、以下のようになります。
#version 120
// aberration.vert
varying vec3 r; // 視線の反射ベクトル
varying vec3 s_r; // 視線の赤方向の屈折ベクトル
varying vec3 s_g; // 視線の緑方向の屈折ベクトル
varying vec3 s_b; // 視線の青方向の屈折ベクトル
varying vec3 f; // 境界面での反射率
したがって屈折率の比も、RGB のそれぞれについて指定しておきます。もともとの屈折率の比は $\eta = 0.67$ でしたから、これを緑の屈折率の比に使い、それより屈折率が低い赤の屈折率の比は多少高めに、屈折率が高い青の屈折率の比は多少低めに設定します。テキトーですね。RGB それぞれの屈折率の比を vec3 にまとめ、垂直入射時の反射率 $F_0$ もまとめて求めておきます。
// 各色の屈折率の比(の逆数)
const vec3 eta = vec3(0.69, 0.67, 0.65);
// 視線ベクトルが垂直方向から入射するときのフレネル反射率
const vec3 f0 = (1.0 - eta) * (1.0 - eta) / ((1.0 + eta) * (1.0 + eta));
そして、それぞれの屈折率の比を用いて、屈折方向のベクトルを求めます。
void main(void)
{
gl_Position = ftransform();
vec4 p = gl_ModelViewMatrix * gl_Vertex; // 頂点位置
vec3 v = normalize(p.xyz / p.w); // 視線ベクトル
vec3 n = normalize(gl_NormalMatrix * gl_Normal); // 法線ベクトル
mat3 m = mat3(gl_TextureMatrix[0]); // テクスチャの回転変換
// 視線の反射ベクトルを求める
r = m * reflect(v, n);
// 視線の屈折ベクトルを求める
s_r = m * refract(v, n, eta.r);
s_g = m * refract(v, n, eta.g);
s_b = m * refract(v, n, eta.b);
// フレネル反射率を求める (Schlickの近似)
f = f0 + (1.0 - f0) * pow(1.0 + dot(v, n), 5.0);
}
フラグメントシェーダプログラム aberration.frag では、バーテックスシェーダプログラムで求めた3つの屈折方向のベクトルを使ってテクスチャをサンプリングし、それぞれ赤成分、緑成分、青成分だけを取り出します。それらをひとつにまとめてから映り込みの色と合成し、フラグメントの色に設定します。
#version 120
// aberration.frag
uniform samplerCube cubemap;
varying vec3 r; // 視線の反射ベクトル
varying vec3 s_r; // 視線の赤方向の屈折ベクトル
varying vec3 s_g; // 視線の緑方向の屈折ベクトル
varying vec3 s_b; // 視線の青方向の屈折ベクトル
varying vec3 f; // 境界面での反射率
void main(void)
{
vec4 c;
c.r = textureCube(cubemap, s_r).r;
c.g = textureCube(cubemap, s_g).g;
c.b = textureCube(cubemap, s_b).b;
c.a = 1.0;
gl_FragColor = mix(c, textureCube(cubemap, r), vec4(f, 1.0));
}
これだけで、こういう結果が得られます。

分光現象を表現するには、厳密には RGB の3つの成分だけでなく、光の周波数(色)分布の変化(偏り)を元に、再現される色を決定する必要があります。しかし、RGB 成分だけで計算しても、それなりにそれらしく見えます。第一、反射マッピングや屈折マッピング自体、とても大胆な近似を行っている(キューブマッピングでは映り込む背景が無限の彼方にあるとか、屈折マッピングでは裏側の境界面による屈折を考慮してないとか)ので、分光現象についてだけ厳密にやっても意味がないでしょう。所詮、CG なんてものは近似のテクニックの塊ですから。
近似するということ
先週のコンピュータグラフィックスの授業でスムーズシェーディングについて解説した時に、「CG ってのは現象をどうモデル化するかとか、そしてそれをどう近似するかってあたりにいろいろ頭を使うところがある」というような話をしました(つもりだけど伝わってないかも)。このあたりのセンスは、CG エンジニアにとって必須のものだと感じます。
私にとって「きれいな CG」というのは、例えば大域照明を使えばリアルな絵になるという話ではなくて、ゴミであれ何であれ、表現したいものを描くために対象をどういう風にモデル化するかを突き詰めて考えている、ということなんだと思います。映像の制作者は、見る者に対してどういう印象を与えるかとか、どういう感情を湧き起こすかということを考えて絵作りをします。WALL·E は、前半ほとんどセリフなしで話が進んでいきますが、それでも強く内容に引き込まれていきます。それはその「絵」にものすごくたくさんのものが詰め込まれているからなんだと思います。CG エンジニアは、そういう絵作りの理想をかなえるために、対象を的確にモデル化し、それを効果的に近似し再現する方法を見つけて、具体的に形にしていく力を持っていなければなりません。
WALL·E を見て、昔、CG を作りたいと思っていた頃の、自分の気持ちを懐かしく思い出しました。