[GLSL] ミルクエフェクトを解読してみる


最近はもっぱら[Filters](http://filters.kayac.com/)で面白そうなフィルターを見つけてForkしてはコードを見ていますｗ

今回はミルクエフェクト（コーヒーにミルクを入れて混ぜた時みたいなぐるぐるしてるやつ）を見てみました。

![Milk effect](https://qiita-image-store.s3.amazonaws.com/0/5616/78c0b121-74f8-c453-8708-229abf7309cd.jpeg "Milk effect")
*↑Mac book Airのキーボード*


## コード

全体のコードはこんな感じ。

```glsl:milk-effect
const float radius     = 600.0;
float input_sin   = 2.0;
float input_power = 8.0;

void main(void) {
    float wave  = (1.0 / iSize);
    float ratio = wave * 0.8;

    // animation
    input_power *= ratio;

    // calculate distance from center
    float _max = max(iResolution.x, iResolution.y);
    float _min = min(iResolution.x, iResolution.y);
    float res_ratio = (_min / _max) / 2.0; // 画面全体になるように補正
    vec2 resolution = iResolution * res_ratio;
    vec2 center2 = (resolution / 2.0);
    vec2 texSize = resolution;

    vec2 tc = iScreen * texSize;

    // 解像度の半分をマイナス＝中央位置
    tc -= center2;

    // move color
    float dist = length(tc);
    if(dist < radius) {
        // 中心位置からどれくらい離れているかの割合
        float percent = (radius - dist) / radius;
        
        // 割合から角度を算出
        float theta = percent * percent * input_power * input_sin;
        float s = sin(theta);
        float c = cos(theta);
        
        // theta分だけ座標を回転
        tc = mat2(c, -s, s, c) * tc;
    }

    // 中心位置に移動していた座標を元に戻す
    tc += center2;

    gl_FragColor = texture2D(iCamera, tc / texSize);
}
```

画面全体にエフェクトがかかるようにしていたり、ピンチイン・ピンチアウトでエフェクトを操作できたり、という手を加えていますが、大事な部分はif文の中にあります。


## 全体像を考える

色々計算していますが、なにをやっているかをまず考えてみましょう。（ちなみに自分もしっかり理解できたわけではありません；）

`float dist = length(tc);`の式から分かるように、長さを計算しています。
また`if (dist < radius) {...}`の分岐からも分かるように、計算座標が事前に決めた半径内に入っている場合に計算をしています。

つまりエフェクト対象範囲の内か外か確認し、外ならなにもしない、というわけですね。


### 参照ピクセルを移動する

さて、ではエフェクトをかける範囲に入っていた場合になにをしているのか。
最後の行がその答えです。`mat2(c, -s, s, c)`を掛けています。これは **回転行列** です。
つまり、その直前で計算された分だけ参照ピクセルの位置を回転している、というわけです。
だからぐるぐるするわけですね。

そして、どれくらい回転させるのかを中心からの距離を係数にして算出しています。
具体的には以下のコード。

```glsl
// 中心位置からどれくらい離れているかの割合
float percent = (radius - dist) / radius;
        
// 割合から角度を算出
float theta = percent * percent * input_power * input_sin;
```

`percent`はそのままの意味ですね。`dist`が半径（`radius`）と同じ場合に`0`、逆に中央`(0, 0)`の場合に`1`、つまり`100%`となる計算です。

そして求めた割合を元に角度を計算しています。
ここはどれくらいの強さで回転させるかの計算なので、`input_power * input_sin`の値を調整することでエフェクトのかかり方を変えることができます。
`percent`の2乗分をそれに掛けているので、半径付近になればなるほど効果が大きくなるわけです。

#### 余談

ちなみにこのべき乗はシェーダを書いているとよく目にします。
シェーダは基本的に`0〜1`（場合によってはマイナス値も）の間の数値でやりとりするため、べき乗を取ると極端に数値が小さくなるケースがあります。

例えば`0.1`の二乗は`0.01`ですが、`0.9`の二乗は`0	.81`と比較的元の数値のままになります。
これを使って表現されるもののひとつにスペキュラがあります。
これはいわゆる金属的な光沢を表現する手法ですが、まさにべき乗を使って反射の強さを調整します。
なんでか二乗、三乗してるなーというときがあったら、数値の強さを調整しているんだな、と覚えておくとコードの理解が進みやすくなります。


### 元に戻す

そして最後に、最初のほうで中央に寄せる目的で引いた数値分足してやります。
さらに`texture2D`関数に渡すUV座標も元に戻します。
（具体的には`texSize`で割る。最初のほうで`texSize`を掛けているので、割ることで元に戻しているわけですね）

こうすることでテクスチャの参照位置が元に戻ります。
もちろん、エフェクトの範囲内だと回転した位置になります。
（仮にif文の中に入らなければ、`center2`を引いて足してるだけなので実質なにもしていないことになります）

あとはいつも通り、求めたUV座標を使ってテクスチャから色をサンプリングすれば完成、となります。

いやー、一行一行しっかり見ていけば意外と理解できたりしますね。
FiltersはGLSLの学習に持って来いなアプリだと思います( *'-')

興味がある人はインストールしてみてください。

[![Filters](https://qiita-image-store.s3.amazonaws.com/0/5616/8b929291-2e54-5b2c-f2bf-4b043eddb73c.png "Filters")](https://itunes.apple.com/jp/app/filters-wu-xianni-zengerufiruta/id926621140)

[![AppStoreへ](https://qiita-image-store.s3.amazonaws.com/0/5616/e9341218-ead6-b198-4c3b-9c6f3fb7ce4d.png "AppStoreへ")](https://itunes.apple.com/jp/app/filters-wu-xianni-zengerufiruta/id926621140)