Akashic Engine でライティング

Akashic Advent Calendar 2019 二十五日目の記事です。

Akashic Engine はフラグメントシェーダを利用できます。これを使ってゲーム画面に様々なポストエフェクトを適用することができます。この記事ではフラグメントシェーダを利用したライティングを実装してみます。

## ゴールの確認

最初に完成形を確認しましょう。

![prettycatinthelight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/1232852a-9787-66c6-f30d-46232dccb4f9.gif)

スポットライトに照らされて、可愛らしい猫の描かれたポスターが浮かび上がっています。

## ライティングの考え方

ライトには点光源（一点から全方向を照らす光源）や平行光源（無限遠にある光源）があります。この記事ではゲームで使いでがありそうなスポットライトを実装します。

ゲーム画面の一部を照らすことを考える時、二次元上の処理で済ませることもできそうですが、ここでは次の図のようにモデル化してみます。

![fig01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/c45f0679-c0c5-1c70-8788-a34d9bfb1827.png)

UV平面はフラグメントシェーダが扱うテクスチャの平面です。これにW軸を追加します。高いところから懐中電灯で照らしている、というイメージです。

スポットライトはある方向を強く照らし、その方向から離れるほど減衰するものです。次の図の、光源から伸びる矢印がスポットライトの方向です。そこから離れるほど減衰し、破線の外側を照らしません。

![fig02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/1577bd30-fda3-6391-0824-affbd10b0e62.png)

スポットライトのパラメータは次のようになります。

- 位置
- 向き
- カットオフ
    - スポットライトの範囲を決める角度。
- 明るさ
- 指数
    - スポットライトの方向から離れるにつれて光を弱くする値。
- 環境光
    - スポットライトの範囲外の明るさ。

具体的な計算はコード上で確認してください。

## 完成

シェーダは次のようになります。

```glsl
#version 100

precision mediump float;

uniform sampler2D uSampler;
varying vec2 vTexCoord;

// テクスチャの解像度。
uniform float image_width;
uniform float image_height;

// スポットライトのパラメータ。
// スポットライトの位置。
uniform float light_pos_x;
uniform float light_pos_y;
uniform float light_pos_z;
// スポットライトの方向。
uniform float light_dir_x;
uniform float light_dir_y;
uniform float light_dir_z;
// スポットライトの明るさ。
uniform float light_intensity;
// スポットライトの範囲。
uniform float light_cutoff;
// スポットライトの指数（大きくなるほど速く減衰する）。
uniform float light_exp;
// 環境光。スポットライトの範囲外の明るさ。
uniform float light_ambient;

// スポットライトの計算
//
// uv: スポットライトで照らされる画像上の位置。
// n: 画像の法線。
// lightPos: スポットライトの位置。
// cutoff: スポットライトの範囲。
// itensity: スポットライトの明るさ。
float spotLighting(vec2 uv, vec3 lightPos, vec3 spotDir, float cutoff, float exponent, float intensity) {
    // 画像上の一点からみた光源の方向。
    vec3 lightDir = normalize(lightPos - vec3(uv, 0.));
    // 画像上の一点に届く光線がスポットライトの方向からどの程度離れているか、を表す量（離れ具合の角度の余弦）。
    float spotCos = dot(spotDir, -lightDir);
    // スポットライトに照らされる範囲にあるか。
    if (spotCos >= cutoff) {
        return max(dot(vec3(0., 0., 1.), lightDir), 0.) // UV平面に対して光線が斜めに当たるほど弱くなるようにする。
             * pow(spotCos, exponent) // スポットライトの向きから外れた光線ほど減衰するようにする。
              * intensity; // スポットライト本来の明るさ。
    } else {
        // スポットライトの範囲外は照らされない。
        return 0.;
    }
}

void main() {
    vec2 uv = vTexCoord;

    vec3 lightPosition = vec3(light_pos_x, light_pos_y, light_pos_z);
    vec3 spotDir = normalize(vec3(light_dir_x, light_dir_y, light_dir_z));
    float intensity = spotLighting(uv, lightPosition, spotDir, light_cutoff, light_exp, light_intensity);

    vec3 ambient = vec3(light_ambient);
    vec3 lightColor = vec3(1., 1., 1.);
    vec4 color = texture2D(uSampler, uv);

    gl_FragColor = vec4(color.rgb * ambient + color.rgb * lightColor * intensity, 1.);
}
```

スポットライトの実装は以上です。デモは[GitHubから入手](https://github.com/blackspotbear/akashic-sample/tree/master/lighting)できます。

## 応用: 立体感を出す

`spotLighting()` の計算の中に `vec3(0., 0., 1.)` という定数があります。これはUV平面がW軸正の方向を向いている、という値です。

この値を変化させると、まるで画像の表面に凹凸があるかのようなライティングが可能になります。次の例では凹凸を画像の輝度から求めています。レンガの壁に立体感がでました。

![prettycatemboss.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/f774500b-a84f-6c88-ed6e-b7ec4497d20f.gif)

## 最後に

この記事ではフラグメントシェーダでスポットライトを実装しました。ホラー系ノベルゲームや、アドベンチャーゲームの推理パートで使えそうですね。