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

# はじめに

今までは空間上に光源が1つしかない状況でしたが、実際に複雑なCGを作ろうと思えば、複数の光源が存在することのほうが普通です。
複数の光源が存在する場合もレンダリングしてみます。

# 1. ある頂点における複数の光源の影響

まずはある頂点における複数の光源の影響を考えてみます。

![スクリーンショット 2018-10-27 23.31.26.png](https://qiita-image-store.s3.amazonaws.com/0/10332/e149e161-0ca1-2160-abac-5fa7011f670c.png)

今までは光源が1つでしたので、ある頂点に当たる光は1つで、その拡散光や反射光を計算していました。
またその光源が空間全体に与える環境光も考慮していました。
光源が2つになったら、ある頂点に当たる光は2つになります。拡散光や反射光も2つの光それぞれに対して発生します。
また2つの光源がそれぞれ空間全体に環境光の影響を与えます。

つまり、光源が複数あるということは、ある頂点において、

* 光源の数だけ拡散光がある
* 光源の数だけ反射光がある
* 光源の数だけ環境光の影響を受ける

ということになります。
それぞれの光源の拡散光、反射光、環境光を個別に計算し、足し合わせることで、その頂点における合計の拡散光、反射光、環境光を得ることができます。

まとめると以下のように考えることができます。

オブジェクトのある頂点のライティング後の色は、

* 光源が1つのとき、
    * `ライティング後の色 = オブジェクト表面色 * 拡散光色 + 環境光色 + 反射光色 + オブジェクト放射光色`
* 光源が複数のとき、
    * `ライティング後の色 = オブジェクト表面色 * sum(拡散光色1 + 拡散光色2 + ...) + sum(環境光色1 + 環境光色2 + ...) + sum(反射光色1 + 反射光色2 + ...) + オブジェクト放射光色`

となります。
(TODO: なんで各パラメータの加算でよいのか？)

# 2. uniform変数を配列として扱う

複数の光源を実装してみます。
光源が複数あるということは、今までuniform変数で設定していた光源のパラメータを、光源の数だけ設定する必要があります。
C++言語であれば光源のパラメータを配列にして、そのインデックスで何番目の光源かを表す手法を取ることができますが、GLSL言語でもuniform変数も配列として扱うことができます。

以下のように、シェーダのuniform変数の宣言で変数名の後ろに配列要素数を指定することで、uniform変数が配列として扱えます。

```glsl
uniform vec3 light_pos[3];
```

C++側でuniform変数を設定する場合は、添字付きのuniform変数名でlocationを指定します。

```cpp
program->SetUniform("light_pos[0]", lightPos1);
program->SetUniform("light_pos[1]", lightPos2);
program->SetUniform("light_pos[2]", lightPos3);
```

GLSLではC++のようなfor文を書くことが可能です。変数iを置いて添字を順番にアクセスすることができます。

```glsl
for (int i = 0; i < 3; ++i)
{
    vec3 light_vec_dir = v_position - light_pos[i];
    // ...
}
```

# 3. プログラムを修正する

光源を3つに増やし、1つ１つuniform変数を設定してきます。

```cpp:Game.cpp
    // ...
    {
        // ライト1
        GLKVector3 lightPos = GLKVector3Make(-1.f, 4.f, 2.0f);
        GLKVector3 spotDir = GLKVector3Make(0.27f, -1.0f, -0.6f);
        program->SetUniform("light_pos[0]", lightPos);
        program->SetUniform("light_attenuation[0]", 0.01f);
        program->SetUniform("spot_dir[0]", spotDir);
        program->SetUniform("spot_phi[0]", GLKMathDegreesToRadians(45.f));
        program->SetUniform("spot_theta[0]", GLKMathDegreesToRadians(30.f));
        program->SetUniform("spot_falloff[0]", 1.f);

        program->SetUniform("diffuse_color[0]", GLKVector4Make(0.9f, 0.1f, 0.1f, 1.0f));
        program->SetUniform("ambient_color[0]", GLKVector4Make(0.3f, 0.0f, 0.0f, 1.0f));
        program->SetUniform("specular_color[0]", GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f));
        program->SetUniform("specular_shininess[0]", 50.0f);
    }

    {
        // ライト2
        GLKVector3 lightPos = GLKVector3Make(-3.f, 4.f, -1.0f);
        GLKVector3 spotDir = GLKVector3Make(0.8f, -1.f, 0.3f);
        program->SetUniform("light_pos[1]", lightPos);
        program->SetUniform("light_attenuation[1]", 0.01f);
        program->SetUniform("spot_dir[1]", spotDir);
        program->SetUniform("spot_phi[1]", GLKMathDegreesToRadians(45.f));
        program->SetUniform("spot_theta[1]", GLKMathDegreesToRadians(30.f));
        program->SetUniform("spot_falloff[1]", 1.f);

        program->SetUniform("diffuse_color[1]", GLKVector4Make(0.1f, 0.9f, 0.1f, 1.0f));
        program->SetUniform("ambient_color[1]", GLKVector4Make(0.0f, 0.1f, 0.0f, 1.0f));
        program->SetUniform("specular_color[1]", GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f));
        program->SetUniform("specular_shininess[1]", 50.0f);
    }

    {
        // ライト3
        GLKVector3 lightPos = GLKVector3Make(3.f, 4.f, 2.0f);
        GLKVector3 spotDir = GLKVector3Make(-0.8f, -1.f, -0.6f);
        program->SetUniform("light_pos[2]", lightPos);
        program->SetUniform("light_attenuation[2]", 0.01f);
        program->SetUniform("spot_dir[2]", spotDir);
        program->SetUniform("spot_phi[2]", GLKMathDegreesToRadians(45.f));
        program->SetUniform("spot_theta[2]", GLKMathDegreesToRadians(30.f));
        program->SetUniform("spot_falloff[2]", 1.f);

        program->SetUniform("diffuse_color[2]", GLKVector4Make(0.1f, 0.1f, 0.9f, 1.0f));
        program->SetUniform("ambient_color[2]", GLKVector4Make(0.0f, 0.0f, 0.1f, 1.0f));
        program->SetUniform("specular_color[2]", GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f));
        program->SetUniform("specular_shininess[2]", 50.0f);
    }

    GLKMatrix4 modelMat = GLKMatrix4Identity;
    modelMat = GLKMatrix4Translate(modelMat, 0.0f, -2.0f, 0.0f);
    modelMat = GLKMatrix4Scale(modelMat, 20.0f, 20.0f, 20.0f);
    program->SetUniform("model_mat", modelMat);
    GLKMatrix4 pvmMat = GLKMatrix4Multiply(projViewMat, modelMat);
    program->SetUniform("pvm_mat", pvmMat);

    program->SetUniform("emissive_color", GLKVector4Make(0.f, 0.f, 0.4f, 0.f));
    mesh->Draw();

    program->SetUniform("emissive_color", GLKVector4Make(0.f, 0.f, 0.0f, 0.f));
    planeMesh->Draw();
```

こういうプログラムの書き方をすると、光源を構造体で定義してまとめて送れないだろうかと思ってきますが、そのような書き方も可能なようです(が、まだglUniform系のメソッドやメモリアラインメントとかの扱いがまだ良くわからないので、今回は愚直なコードで書きます。)

# 4. シェーダを修正する

頂点シェーダに変更はありません。
フラグメントシェーダーでfor文でライティングの計算を行います。

```glsl:myshader.fsh
#version 410

in vec3 v_position;
in vec3 v_normal;
in vec4 v_color;
const int LIGHT_NUM = 3;
uniform vec3 light_pos[LIGHT_NUM];
uniform float light_attenuation[LIGHT_NUM];
uniform vec3 spot_dir[LIGHT_NUM];
uniform float spot_phi[LIGHT_NUM];
uniform float spot_theta[LIGHT_NUM];
uniform float spot_falloff[LIGHT_NUM];
uniform vec3 eye_dir;
uniform mat4 pvm_mat;
uniform mat4 model_mat;
uniform vec4 diffuse_color[LIGHT_NUM];
uniform vec4 ambient_color[LIGHT_NUM];
uniform vec4 specular_color[LIGHT_NUM];
uniform float specular_shininess[LIGHT_NUM];
uniform vec4 emissive_color;
layout (location=0) out vec4 frag_color;

void main()
{
    vec4 diffuse_total = vec4(0);
    vec4 ambient_total = vec4(0);
    vec4 specular_total = vec4(0);
    for (int i = 0; i < LIGHT_NUM; ++i)
    {
        vec3 light_vec_dir = v_position - light_pos[i];
        float light_len = length(light_vec_dir);
        float attenuation = 1.0 / (light_attenuation[i] * light_len * light_len);

        vec3 light_vec_dirN = normalize(light_vec_dir);
        vec3 spor_dirN = normalize(spot_dir[i]);
        float cos_alpha = dot(light_vec_dirN, spor_dirN);
        float cos_half_theta = cos(spot_theta[i] / 2.0);
        float cos_half_phi = cos(spot_phi[i] / 2.0);
        if (cos_alpha <= cos_half_phi)
        {
            // out-range
            // attenuation * 0.f;
            ambient_total += ambient_color[i];
        }
        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[i]);
            }
            vec3 normal = normalize((vec4(v_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[i]);

            diffuse_total += diffuse_color[i] * diffuse_power * attenuation;
            ambient_total += ambient_color[i];
            specular_total += specular_color[i] * specular;
        }
        frag_color = v_color * diffuse_total + ambient_total + specular_total + emissive_color;
    }
}

```

3つの方向から赤、緑、青のスポットライトを浴びたカラフルなstanford bunnyが描画されました。

![スクリーンショット 2018-10-27 22.43.49.png](https://qiita-image-store.s3.amazonaws.com/0/10332/2ca2d541-d8a5-5c9a-6073-ee6234e94068.png)

各光源のライティングの計算はオブジェクト単位で行っているため、bunnyの下のあたりのplaneは本来光の当たらない場所ではあるのですが、遮蔽物を考慮しないことで逆にスポットライトの光が混じり合う様子がよく見えています。

※[macOSでOpenGLを勝手に勉強する(目次)](https://qiita.com/uechoco@github/items/655806d9252c035f9efc)

参考文献

* [Help with Gouraud/Phong Shading in Shaders](https://www.opengl.org/discussion_boards/showthread.php/185829-Help-with-Gouraud-Phong-Shading-in-Shaders)
* [Modern OpenGL 08 – Even More Lighting: Directional Lights, Spotlights, & Multiple Lights — Tom Dalling](https://www.tomdalling.com/blog/modern-opengl/08-even-more-lighting-directional-lights-spotlights-multiple-lights/)