【Swift】MetalシェーダーでLerpフィルターを実装する【Metal】【線形補間】

# ■ 概要
本記事では、

**Swiftの「Metal」における、
画像を2色間で線形補間（Lerp）するシェーダー実装**

について紹介したいと思います。

# ■ アウトプット
「2色間で線形補間（Lerp）する」ことによってどのような見た目になるか。


**元画像（iPadの画面に表示）：**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730387/294cc12c-8df2-118f-ee1d-8c1c4e82735e.png" width="400">

**↓**

**実装後の画像（iPadの画面に表示）：**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730387/18abaa09-0ba0-1502-0230-38ab6da945f2.png" width="400">

かわいい。

---

このように、画像の各ピクセルの色情報を取得し、指定した2色の間で補間をします。

今回の例では、```#3A94FF```と```#D5FCFF```の2色のカラーを指定。

黒（```#000000```）に近いほど濃いめの青（```#3A94FF```）に、
白（```#FFFFFF```）に近いほど薄めの青（```#D5FCFF```）になるように補間します。

# ■ 環境

Xcodeバージョン　：　15.3

# ■ 実装

まずは実装から。

今回はシェーダーファイルの実装のみを載せるので、
CPUプログラム側の実装は適宜実装をしてみてください。

```swift:ColorLerpShader.metal
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;

struct Out {
  float4 position [[ position ]];
  float2 texCoord [[ user(texturecoord) ]];
};

vertex Out colorFilterVertexShader(uint vid [[ vertex_id ]],
                          const device float2 *position [[ buffer(0) ]],
                          const device float2 *texCoord [[ buffer(1) ]])
{
  Out out;
  out.position = float4(position[vid], 1.0);
  out.texCoord = texCoord[vid];
  return out;
}

fragment float4 colorFilterFragmentShader(Out vert [[ stage_in ]],
                                          texture2d<float, access::sample> inputTexture [[ texture(0) ]],
                                          // 開始色（黒色との補間）のカラーバッファ
                                          constant float4 *startColor [[ buffer(0) ]],
                                          // 終了色（白色との補間）のカラーバッファ
                                          constant float4 *endColor [[ buffer(1) ]])
{
    constexpr sampler s(mip_filter::none,
                        mag_filter::linear,
                        min_filter::linear,
                        address::clamp_to_edge);
                        
    // インプットテクスチャからピクセルの色情報を読み取る
    float4 color = inputTexture.sample(s, vert.texCoord);

    // RGB値をグレースケールに変換する
    float luminance = dot(color.rgb, float3(0.299, 0.587, 0.114));
    
    // グレースケールの値をもとにLerp(線形補間)する
    float4 lerpedColor = mix(*startColor, *endColor, luminance);
    
    return lerpedColor;
}
```

# ■ 解説

ポイント（要所）のみ解説していきます。

### 1. グレースケール変換

```Swift
// RGB値をグレースケールに変換する
float luminance = dot(color.rgb, float3(0.299, 0.587, 0.114));
```

フラグメントシェーダー内、取得した元画像の色情報を「グレースケール変換」します。
この工程は後述の線形補間のために必要で、ここで得たグレースケール値が
そのまま補間のパラメーターになります。

**ここでは「dot()関数」で、取得した元画像の色情報を、
特定の比率（重み付け）で加重平均しています。**

---

グレースケール変換のアプローチとして、ピクセル毎にそのピクセルのrgb値それぞれの値を
加算して、その合計を3で割ればグレースケール化できますが、rgb値にそれぞれ0.33を
掛け、それらを合計しても同様の結果が得られます。

↓

さらに、グレースケール変換における加重平均を適用するために
`dot()関数`(ベクトルの内積)を使うのが便利です。

<font color="gray">(グレースケール変換における加重平均については[こちらの記事が参考になります。](https://talavax.com/grayscale2.html#gsc.tab=0))</font>


### 2. Lerp（線形補間）

```Swift
// グレースケールの値をもとにLerp(線形補間)する
float4 lerpedColor = mix(*startColor, *endColor, luminance);
```

フラグメントシェーダー内、グレースケール値をもとに2色間で線形補間します。
`mix()関数`はLerp（線形補間）と同じ処理結果を返す関数で、名前が違うだけです。

アルゴリズムとしてはこんな感じ。自前でも実装できます。
```Swift
float4 lerp(float4 startColor, float4 endColor, float luminance) {
    return startColor + (endColor - startColor) * luminance;
}
```

**→ 第1引数、第2引数にはそれぞれ補完する2色を引き渡します。**

第1引数には開始色（黒と補間する色、今回の例では```#3A94FF```）
第2引数には終了色（白と補間する色、今回の例では```#D5FCFF```）



**→ 第3引数には補間のパラメーターを、0.0~1.0の間のfloatで引き渡します。**

今回の例では、

「0.0」→ ```#3A94FF```
「1.0」→ ```#D5FCFF```
「0.5」→ 2色の中間の色```#87C8FE```

といった具合で補間されます。

つまり、この第3引数にグレースケール値の0.0~1.0の値をそのまま引き渡せば
2色間で線形補間された色情報が返されます。


# ■ 最後に （感想）

本当はCIFilter等で簡単に似たような処理ができれば良かったのですが、
CIFilterには線形補間フィルターのプリセットが用意されていなかったため
Metalシェーダーで実装してみました。

最後まで読んでいただきありがとうございました。