点光源のライティングを実装する(4-7. 相当)


はじめに

この記事では、点光源をライティングに取り込み、光源からの距離によって光が減衰していく様子を見ていきます。
4-5の記事で実装した平行光源は、無限遠から指向性のある減衰しない光を表していました。一方で点光源は3D空間上に光源が位置し、有限の距離からの光を表します。点光源の光は球状に均等に拡散していき、光源から遠ざかると光が減衰します。これを「距離減衰」と呼びます。

1.光源ベクトルの向き

平行光源は無限遠にある光源から各頂点に向かって光が降り注ぐため、どの頂点でも光の向き(光源ベクトル)は同じ向きを表していました。
一方で、点光源は有限の距離にある光源から各頂点に向かって光が当たるため、頂点と光源を結んだ光源ベクトルは、各頂点で異なる値になります。

光源ベクトルの向きを計算するのは非常に簡単で、 頂点の位置 - 点光源の位置 となります。

2.距離減衰

点光源の特徴である距離減衰について、どのくらい距離があるとどのくらい光が減衰してしまうかを考えてみます。
点光源は光が球状に均等に拡散するため、光源から一定距離離れた球面上に拡散した光量の合計と、拡散する前の点の状態の光量は等しいと考えられます。つまり光源から距離 r 離れた点に届く光量は $ attenuation = \frac{1}{4 \pi r^2}$ に減衰します。「距離の2乗に反比例して減衰する」と言えます。

ただ、ライティングというのは光のシミュレーションですので、好みのレンダリング結果になればどうシミュレーションしてもよいわけで、距離rに反比例して減衰する線形減衰 $ attenuation = \frac{1}{ar} $ として実装したり、組み合わせて $ attenuation = \frac{1}{k_0 + k_1 r + k_2 r^2} $ として実装されることもあります。特に後者の式は、OpenGLやDirectXで標準的に用いられている減衰係数の計算式です。

3. 平面の配置

それでは点光源を実装していきます。まずは点光源の光が当たる様子をわかりやすくするために、stanford bunnyの下に平面オブジェクトを配置します。
plane.objをダウンロードし、Xcodeのナビゲーションツリー上のResourcesフォルダにに追加してください。

Gameクラスのコンストラクタで平面オブジェクトを読み込み、デストラクタで解放します。

Game.h
   Mesh*          planeMesh = nullptr;
Game.cpp
Game::Game()
{
  ...
  planeMesh = new Mesh("plane.obj");
  ...
}
Game::~Game()
{
  ...
  delete planeMesh;
  ...
}

また描画部の最後に、bunnyと一緒に平面オブジェクトも描画します。

Game.cpp
void Game::Render()
{
  ...
  planeMesh->Draw();
}

4. 点光源の実装

平行光源の光源ベクトル light_dir のUniform変数の設定を削除し、代わりに点光源の位置と減衰係数を設定します。

Game.cpp
void Game::Render()
{
  ...
  GLKVector3 lightPos = GLKVector3Make(-1.f, -1.f, 2.0f);
  program->SetUniform("light_pos", lightPos);
  program->SetUniform("light_attenuation", 0.9f);
  ...
}

次に頂点シェーダーを変更します。

まずは削除したUniform変数 light_dir の代わりに、 light_poslight_attenuation を定義します。

myshader.vsh
...
uniform vec3 light_pos;
uniform float light_attenuation;
...

光源ベクトル、光源までの距離、距離減衰係数を求めます。今回は距離の二乗に反比例するシミュレーションで実装します。
オブジェクトの頂点はモデル行列で変換後の実際の位置を求めてから、光源の位置を引きます。
(ネット上のサンプルでは、ligh_dirを光の向きのベクトルとして扱うものと、光源へのベクトルとして扱うものの両方が見受けられます。今回は光の向きのベクトルという意味でlight_dirを扱います。)

  ...
  gl_Position = vec4(vertex_pos, 1.0) * pvm_mat;

  vec3 light_dir = (vec4(vertex_pos, 1.0) * model_mat).xyz - light_pos;
  float light_len = length(light_dir);
  float attenuation = 1.0 / (light_attenuation * light_len * light_len);

  vec3 normal = normalize((vec4(vertex_normal, 0.0) * model_mat).xyz);
  ...

最後に、拡散反射光の計算式にに減衰係数をかけ合わせます。

  color = vertex_color * diffuse_color * diffuse_power * attenuation + ambient_color + specular_color * specular;

描画結果は以下のようになりました。

カメラの位置によっては平面上に変なスペキュラーが出てしまうのですが、ちょっとよくわからないので一旦無視しちゃいます。
平面上の点光源が割となめらかに表示されていますが、実はplane.objは平面のメッシュをかなり細分化しているので、フラグメントシェーダーがピクセルカラーを線形補間してもきれいに描画されています。以下のように平面のメッシュが粗いと、その粗さが描画結果にも現れてしまいます。

メッシュの粗さにかかわらずきれいな描画結果を得るには、頂点シェーダーではなくピクセルシェーダーにライティングの処理を書き、ピクセル単位で光の当たり具合を計算することを検討したほうが良さそうです。実際に点光源に関するネット上の記事の多くは、ピクセルシェーダーにライティングの処理を記述しています。

ピクセルシェーダーによるライティングは後ほど記事にします。

macOSでOpenGLを勝手に勉強する(目次)

参考資料