はじめに
この記事では、平行光源から来た光が物体表面で反射して、その光が直接目に入ってくる**「反射光」**を、ライティングに取り入れてみます。
これまで見てきた平行光源の拡散光と環境光を使ったライティングでは、物体が固定されている場合、その物体をどこから見ても、すべての面が同じ光り方しかしませんでした。しかし現実世界では、物体の表面に当たった光は、一部はこれまで通り拡散されますが、一部は反射して、入射角と等しい反射角で跳ね返ります。ビリヤードと同じ要領です。
こうして反射した光がどのように見えるかは、目の位置とその向きによって、大きく変わります。拡散光はザラザラしている(と仮定した)物体の表面で四方八方に拡散されているので、どこから見ても光の強さは変わらないのですが、反射光は光子の向かう方向が一方向ですので、その光がまっすぐ目に飛び込んで来た時には拡散光に光の色が足されて明るく見え、少しでも視線が逸れた場合にはほとんど影響がなくなって、拡散光だけの色になります。
身の回りにある物体を見回して、強くハイライトが当たっている箇所と、自分がいる部屋の強い光源の位置との関係を考えてみてください。物体の基本の色が拡散光によって出ていて、強く光っている箇所はその光源が物体に反射して目に直接飛び込んできて白くなっているはずです。
今回追加する反射光のライティングは、視線の向きと反射光の向きがどれだけ一致しているかを数値化することで、反射光が強く見える面の光の色を強くする手法になります。
1. 確認のためにカメラを回転させる
まず、反射光の影響が分かりやすいように、カメラを自動的に回転するようにしてみましょう。これまでの平行光源の拡散光と環境光だけでライティングしているウサギのモデルを固定させて、その周辺をカメラが回転するように、カメラの位置を変更するコードを追加します。次のように、コサインとサインを使ってカメラ位置を計算します。
void Game::Render()
{
cameraPos.x = -cosf(Time::time * 0.6f) * 5.0f;
cameraPos.z = sinf(Time::time * 0.6f) * 5.0f;
...
}
平行光源からの光の反射光なしの場合 pic.twitter.com/zbu77MvOCa
— 風のさざめき (@sazameki) 2018年1月21日
こうして実行してみると、拡散光と環境光だけのライティングでは視線の方向や、目に入ってくる光というものを考慮していませんので、回転しても、同じ面はずっと同じ明るさのままだということが分かります。
ここまでのプロジェクト:MyGLGame_step4-5a.zip
2. Phong の反射モデルの実装
それでは、頂点シェーダを修正し、頂点の色を決定する際に、視線の向きと反射光の一致度合いを計算するコードを追加しましょう。
「GLSLによるフォンシェーディング」のページによくまとめられていますが、この実装方法はいろいろ提案されています。
1975年に Bui Tuong Phong 氏がユタ大学における博士論文としてまとめたとされるのが、Phong の反射モデルです。この論文は、現在こちらで参照することができます。このモデルにおいては、平行光源の光線ベクトルに対して、法線ベクトルを元に反射ベクトルを計算し、反射ベクトルと視線ベクトルのドット積(つまりどれだけこの2つのベクトルの方向が近いかを示す値)として、反射光の明るさを計算します。なお、反射光が強く収束するか、拡散するかを決定するために、計算された明るさはハイライトの強さを表す数値の分だけべき乗して使います。
それでは、この方法を実装してみましょう。まず、頂点シェーダにコードを追加します。
#version 410
layout (location=0) in vec3 vertex_pos;
layout (location=1) in vec3 vertex_normal;
layout (location=2) in vec4 vertex_color;
uniform vec3 light_dir;
uniform vec3 eye_dir;
uniform mat4 pvm_mat;
uniform mat4 model_mat;
uniform vec4 diffuse_color;
uniform vec4 ambient_color;
uniform vec4 specular_color;
uniform float specular_shininess;
out vec4 color;
void main()
{
gl_Position = vec4(vertex_pos, 1.0) * pvm_mat;
vec3 normal = normalize((vec4(vertex_normal, 0.0) * model_mat).xyz);
vec3 light = normalize(light_dir);
float diffuse = clamp(dot(normal, -light), 0.0, 1.0);
vec3 eye = -normalize(eye_dir);
vec3 light_ref = reflect(light, normal);
float specular = pow(clamp(dot(eye, light_ref), 0.0, 1.0), specular_shininess);
color = vertex_color * diffuse_color * diffuse + ambient_color + specular_color * specular;
}
まず、反射して目に入ってくる光の色を表す specular_color
変数と、反射した光と視線の向きの一致度合いがどれだけ強く判定されるかを示す specular_shininess
変数を追加しました。
そして「vec3 eye = -normalize(eye_dir);
」として、視線の逆ベクトルを求めます。正規化しているのは念のため。逆ベクトルにしているのは、向きの一致度を計算するためにドット積を使う場合には、ベクトルのお尻の位置を合わせなければいけないためです。
次に、「vec3 light_ref = reflect(light, normal);
」として、法線ベクトルを中心に、光線ベクトルが反射した反射ベクトルを計算します。reflect()
関数は GLSL の組み込み関数で、第1引数のベクトルを、第2引数の法線ベクトルを中心に反射させたベクトルを計算してくれます。
こうして求めた視線の逆ベクトルと光線の反射ベクトルのドット積を計算することで、反射光の明るさが求まります。clamp()
関数で 0.0〜1.0 の範囲に収まるように調整し、pow()
関数で specular_shininess
変数の分だけべき乗を計算します。
最後に、描画コードを修正して、specular_color
変数と specular_shininess
変数の値を設定しましょう。基本的には拡散光と同じ平行光源から来た光を使って反射光を計算しますので、そう考えると specular_color
変数と diffuse_color
変数の値は同じになるかと思いますが、目に光が飛び込むとそれ以上に明るく見えると考えて、specular_color
変数の値には (1, 1, 1, 1) の白い色を設定しておきます。specular_shininess
変数には、まずは 10 を指定してみます。
void Game::Render()
{
...
program->SetUniform("specular_color", GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f));
program->SetUniform("specular_shininess", 10.0f);
glDrawElements(GL_TRIANGLES, (GLsizei)data.size(), GL_UNSIGNED_SHORT, (void *)0);
}
これを実行すると、次のようになります。とくに背中の辺りの光り方に注目すると、視点の変化に伴って、物体表面で反射した光が面の色に影響するようになったことがよく分かります。このように、視点の変化に合わせて見え方が変わるので、反射光をつけるだけで、これまで静止していた世界が動き始めたような印象が出てきます。
鏡面反射の計算。Phong の反射モデル (1975)。 pic.twitter.com/9HCIhhzEPP
— 風のさざめき (@sazameki) 2018年1月21日
ここまでのプロジェクト:MyGLGame_step4-5-phong.zip
3. Blinn-Phong のシェーディング・モデルの実装
次に、Blinn-Phong のシェーディング・モデルと呼ばれるモデルを実装してみましょう。このモデルは、James Blinn 氏(通称、Jim Blinn 氏)が、1975年のPhong 氏の反射モデルを、2年後の1977年に改良したものです。この論文は、ACMのサイト から購入できます。
このモデルでは、反射ベクトルを計算する代わりに**「ハーフベクトル」**というものを計算します。視線の逆ベクトルと光線の逆ベクトルを単純に足して正規化すると、ちょうど中間の向きを表すベクトル(ハーフベクトル)が計算できるのです。このハーフベクトルが法線ベクトルの向きとどれだけ合致しているかをドット積で計算すれば、それだけで反射光の明るさが計算できます。
それでは、このモデルを実装してみましょう。ハーフベクトルを表す half_vec
変数の計算を追加し、specular
変数の計算を「視線ベクトルと反射ベクトルのドット積」から「法線ベクトルとハーフベクトルのドット積」に変えるだけです。
void main()
{
gl_Position = vec4(vertex_pos, 1.0) * pvm_mat;
vec3 normal = normalize((vec4(vertex_normal, 0.0) * model_mat).xyz);
vec3 light = -normalize(light_dir);
float diffuse = clamp(dot(normal, light), 0.0, 1.0);
vec3 eye = -normalize(eye_dir);
vec3 half_vec = normalize(light + eye);
float specular = pow(clamp(dot(normal, half_vec), 0.0, 1.0), specular_shininess);
color = vertex_color * diffuse_color * diffuse + ambient_color + specular_color * specular;
}
鏡面反射の計算。Blinn-Phong のシェーディング・モデル (1977)。 pic.twitter.com/IBKQO2p8Ll
— 風のさざめき (@sazameki) 2018年1月21日
ここまでのプロジェクト:MyGLGame_step4-5-blinn-phong.zip
こうして実行してみると、Phong の反射モデルよりも、Blinn-Phong のシェーディング・モデルの方が、少し明るめに反射光が出ることが分かります。反射ベクトルの計算と、正規化によるハーフベクトルの計算はそれほど大きくは計算量は変わりませんが、ハーフベクトルの計算の方が多少(ドット積1回分程度)軽くなっています。
ウサギを後ろから見ると、反射光の出方の違いがよく分かります。左が Phong の反射モデル、右が Blinn-Phong のシェーディング・モデルです。Blinn-Phong のシェーディング・モデルの方が、背中に広がる拡散光が大きくなっています。
4. まとめ
この記事では、平行光源から反射した光が目に入ってくる場合のことを考えて、反射光という新しいライティングを追加する方法を説明しました。反射光の計算には、Phong の反射モデルやBlinn-Phong のシェーディング・モデルなどがありますが、ハーフベクトルの計算を使った Blinn-Phong のシェーディング・モデルの実装の方が多く見られると思います。
英語版の Wikipedia の「Specular highlight」のページによくまとめられていますが、鏡面反射のモデルはさらにいろいろと提案されていますので、ぜひいろいろと実装してみてください。
Blinn-Phong のシェーディング・モデルにおいて、specular_shininess
変数の値を小さくすればするほど、反射光が目に入ったことになる角度が大きくなって反射光が見える範囲が広くなり、値を大きくすればするほど、角度が小さくなって反射光が見える範囲が狭くなります。次の図は、左から順に、specular_shininess
変数の値を 1.0, 5.0, 10.0, 50.0 に変化させたものです。
光が当っている物体を回転させても、だいたい反射光は同じ位置にハイライトを作るため、あまり反射光の影響は分かりません。反射光の影響を分かりやすく観察するためには、平行光源の向きを変えるか、カメラの位置を変えてやると良いでしょう。
ここまでで解説した、拡散光・環境光・反射光の3種類の光の計算が、現在の 3DCG におけるもっとも基本的なライティングの計算です。さらに物体表面の粗さを考慮したり、ミクロに見ると物体表面で吸収された光が内部で屈折してまた放出されたりといったことを考慮していくと、より細かなライティング計算ができるのですが、ここまでの実装だけでも、かなりリアルに見えるようになります。(実際にはこれらの計算は、頂点シェーダではなくフラグメント・シェーダで行うべきなのですが、その意味と実装方法については、先の方の記事で扱うことにしましょう。)
ここまでの GLSL におけるライティング計算を見てきて、その基礎はベクトルの方向を合わせた上でドット積を計算し、ベクトル同士がどれだけ近い方向を向いているかを計算することだということが分かっていただけたと思います。