スポットライトのライティングを実装する(4-8. 相当)


はじめに

前回は点光源を実装しましたが、今回は スポットライト によるライティングを実装してみます。

点光源のサンプルはネット上にいっぱい転がってるんですが、スポットライトのサンプルは全然見つからないです、、、なんででしょうね。

スポットライトの光源は、懐中電灯や車のヘッドライトのような光を表すのに使われます。点光源とは違い、全方位を照らすのではなく、特定方向の特定範囲を円形に照らします。点光源と同様に光の色、光源の位置を定義し、光源の焦点からの距離によって光は減衰します。

(inner-corn、outer-cornについての説明は自信ないです、、、)
実際の懐中電灯は、光源が極小点ではなく、球体のような形をしており、光源に幅があります。この球体内のすべての点からある平面に向かって円錐形に光を照射すると、どの点からも光が届く部分は均等に一番明るい色になりますが、そのエッジ部分にはすべての点から光が届かないため、徐々にぼやけていきます。この均等に一番軽い部分をinner-corn、エッジ部分のぼやけて光が届いている部分をouter-cornと呼びます。またエッジ部分の減衰をフォールオフと呼びます。

1. スポットライトのパラメータ

以上のスポットライトの特徴をパラメータ化して定義していきます。

  • vec_pos ... オブジェクトの頂点
  • light_vec_dir ... 光源の焦点から頂点へのベクトル
  • spot_dir ... 光源の向き(光源の焦点からの方向ベクトル)
  • θ theta ... inner-cornの範囲を表す角度(radian) ( $ 0 \leq \theta < \pi$ )
  • φ phi ... outer-cornの範囲を表す角度(radian) ( $ \theta \leq \phi < \pi$ )
  • α alpha ... light_vec_dirspot_dir の角度

また、図には記載がありませんが、inner-cornの端からouter-cornの端にかけてエッジ部分の光が減衰していく具合を制御するための係数として、減衰係数 falloff を導入します。

  • falloff ... inner-cornとouter-cornの間での光の減衰具合を制御する係数
    • 線形に減衰させる場合は falloff = 1.0 になります。

なお、点光源の実装時に定義した以下のパラメータは引き続き使用します。

  • light_pos ... 点光源の位置
  • light_attenuation ... 距離減衰係数

2. ライティングの対象範囲に含まれるか検査する

これまでのサンプルでは頂点シェーダでライティングの計算を行っているので、スポットライトも引き続き頂点シェーダで計算することにします。
スポットライト光源は光の届く範囲が決まっているので、頂点に対してライティングの計算が必要かどうかをチェックし、光が届かないのであれば計算を行わないようにすることができます。これは頂点がouter-cornの領域外かどうかをチェックすれば良さそうです。具体的には
$$ \alpha \geq \frac{\phi}{2} $$
の場合に頂点がouter-cornの外側にあります。
αは light_vec_dirspot_dir の角度ですので、内積の公式から
$$ cos\alpha = \frac{\overrightarrow{spotDir}\cdot\overrightarrow{lightVecDir}}{||\overrightarrow{spotDir}||~||\overrightarrow{lightVecDir}||} $$
となります。acosを取ればαの値は求まりますが、 $ \theta < \pi $ ( $ \frac{\theta}{2} < deg2rad(90^\circ) $ )の範囲においては、角度が大きくなるほどコサインの値が小さくなっていくことさえ覚えておけば、コサインの値のまま対象領域かどうかの判定をすることが可能です。つまり
$$ cos\alpha \leq cos\left(\frac{\phi}{2}\right) $$
の式でも頂点がouter-cornの外側であると言えます。

同様に頂点がinner-cornの内側にあるかどうかをチェックするには
$$ \alpha < \frac{\theta}{2} $$
つまり
$$ cos\alpha > cos\left(\frac{\theta}{2}\right) $$
の式で確認ができます。

頂点がinner-cornの内側にあるということは、光源から届く光量が距離減衰した分だけ届いているといえます。

inner-cornとouter-cornの間の減衰を計算する

inner-cornとouter-cornの間に頂点がある場合、光源から届く光量が距離減衰した分だけではなく、さらにフォールオフ分の減衰が発生しています。
減衰は、αが $ \frac{\theta}{2} $ から遠ざかって $ \frac{\phi}{2} $ に近づくほど減衰していきます。また減衰度合いを falloff 乗で表現します。

(なんでコサインの比で求まるんだろう??)
$$ \left[ \frac{ cos\alpha - cos \left(\frac{\phi}{2}\right) }{ cos\left(\frac{theta}{2}\right) - cos \left(\frac{\phi}{2}\right) } \right]^{falloff} $$

3. 実装する

スポットライト光源の実装をしていきます。
最初に定義したパラメータのうち、CPUからシェーダに渡すものをUniform変数で定義します。

Game.cpp
    GLKVector3 lightPos = GLKVector3Make(-1.f, 1.f, 2.0f);
    GLKVector3 spotDir = GLKVector3Make(0.25f, -1.f, -0.25f);
    program->SetUniform("light_pos", lightPos);
    program->SetUniform("light_attenuation", 0.1f);
    program->SetUniform("spot_dir", spotDir);
    program->SetUniform("spot_phi", GLKMathDegreesToRadians(45.f));
    program->SetUniform("spot_theta", GLKMathDegreesToRadians(30.f));
    program->SetUniform("spot_falloff", 1.f);

次に頂点シェーダーを変更します。
変更量が多いので全文を掲載します。

myshader.vsh
#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_pos;
uniform float light_attenuation;
uniform vec3 spot_dir;
uniform float spot_phi;
uniform float spot_theta;
uniform float spot_falloff;
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 light_vec_dir = (vec4(vertex_pos, 1.0) * model_mat).xyz - light_pos;
    float light_len = length(light_vec_dir);
    float attenuation = 1.0 / (light_attenuation * light_len * light_len);

    vec3 light_vec_dirN = normalize(light_vec_dir);
    vec3 spor_dirN = normalize(spot_dir);
    float cos_alpha = dot(light_vec_dirN, spor_dirN);
    float cos_half_theta = cos(spot_theta / 2.0);
    float cos_half_phi = cos(spot_phi / 2.0);
    if (cos_alpha <= cos_half_phi)
    {
        // out-range
        // attenuation * 0.f;
        color = ambient_color;
        return;
    }
    else
    {
        if (cos_alpha > cos_half_theta)
        {
            // inner corn
            // attenuation * 1.f
        }
        else
        {
            // outer corn
            attenuation *= pow((cos_alpha - cos_half_phi)/(cos_half_theta - cos_half_phi), spot_falloff);
        }
        vec3 normal = normalize((vec4(vertex_normal, 0.0) * model_mat).xyz);
        vec3 light = -light_vec_dirN;
        float diffuse_power = 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_power * attenuation + ambient_color + specular_color * specular;
    }
}

スポットライトの当たらない頂点には、環境光だけを適用します。
inner-cornの内側にある頂点には、距離減衰のみを適用します。
inner-cornとouter-cornの間にある頂点には距離減衰とフォールオフの減衰の両方を適用します。

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

参考資料