■ 概要
本記事では、
Swiftの「Metal」における、
画像を2色間で線形補間(Lerp)するシェーダー実装
について紹介したいと思います。
■ アウトプット
「2色間で線形補間(Lerp)する」ことによってどのような見た目になるか。
元画像(iPadの画面に表示):
↓
実装後の画像(iPadの画面に表示):
かわいい。
このように、画像の各ピクセルの色情報を取得し、指定した2色の間で補間をします。
今回の例では、#3A94FFと#D5FCFFの2色のカラーを指定。
黒(#000000)に近いほど濃いめの青(#3A94FF)に、
白(#FFFFFF)に近いほど薄めの青(#D5FCFF)になるように補間します。
■ 環境
Xcodeバージョン : 15.3
■ 実装
まずは実装から。
今回はシェーダーファイルの実装のみを載せるので、
CPUプログラム側の実装は適宜実装をしてみてください。
#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. グレースケール変換
// RGB値をグレースケールに変換する
float luminance = dot(color.rgb, float3(0.299, 0.587, 0.114));
フラグメントシェーダー内、取得した元画像の色情報を「グレースケール変換」します。
この工程は後述の線形補間のために必要で、ここで得たグレースケール値が
そのまま補間のパラメーターになります。
ここでは「dot()関数」で、取得した元画像の色情報を、
特定の比率(重み付け)で加重平均しています。
グレースケール変換のアプローチとして、ピクセル毎にそのピクセルのrgb値それぞれの値を
加算して、その合計を3で割ればグレースケール化できますが、rgb値にそれぞれ0.33を
掛け、それらを合計しても同様の結果が得られます。
↓
さらに、グレースケール変換における加重平均を適用するために
dot()関数(ベクトルの内積)を使うのが便利です。
(グレースケール変換における加重平均についてはこちらの記事が参考になります。)
2. Lerp(線形補間)
// グレースケールの値をもとにLerp(線形補間)する
float4 lerpedColor = mix(*startColor, *endColor, luminance);
フラグメントシェーダー内、グレースケール値をもとに2色間で線形補間します。
mix()関数はLerp(線形補間)と同じ処理結果を返す関数で、名前が違うだけです。
アルゴリズムとしてはこんな感じ。自前でも実装できます。
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シェーダーで実装してみました。
最後まで読んでいただきありがとうございました。