自作のカラフルな流体っぽいシェーダを解説する

# はじめに

こんなシェーダを書きました。

https://www.shadertoy.com/view/lfKyRw

割と良い感じの綺麗な絵になっているのではと思います。
このシェーダをどうやって作ったのかを説明していきます。

# ①アイデアの原型
アイデアの原型になっているのは弊社CTOがこちらで作成したシェーダです。

https://qiita.com/hiranuma/items/6e7cc424232d68e1e250

![capture (7).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/3c4e43d2-6841-eed0-cc51-2ec78eafd95f.gif)

## 原型のコード

かなりシンプルな記述でこれだけカラフルで違和感のない絵が構成されています。
0→1苦手なのでとても勉強になる...
```glsl
// 7色のグラデーションカラーを定義
vec3 colorA = vec3(1.0, 0.2, 0.2); // Red
vec3 colorB = vec3(0.93, 0.47, 0.0); // Orange
vec3 colorC = vec3(1.0, 0.87, 0.0); // Yellow
vec3 colorD = vec3(0.0, 0.66, 0.37); // Green
vec3 colorE = vec3(0.2, 0.6, 1.0); // Blue
vec3 colorF = vec3(0.61, 0.45, 0.7); // Purple
vec3 colorG = vec3(0.87, 0.52, 0.64); // Pink

// 時間に基づいてグラデーションカラーを返す関数
vec3 getColor(float t) {
    t = fract(t);
    if (t < 1.0/7.0) return mix(colorA, colorB, t * 7.0);
    if (t < 2.0/7.0) return mix(colorB, colorC, (t - 1.0/7.0) * 7.0);
    if (t < 3.0/7.0) return mix(colorC, colorD, (t - 2.0/7.0) * 7.0);
    if (t < 4.0/7.0) return mix(colorD, colorE, (t - 3.0/7.0) * 7.0);
    if (t < 5.0/7.0) return mix(colorE, colorF, (t - 4.0/7.0) * 7.0);
    if (t < 6.0/7.0) return mix(colorF, colorG, (t - 5.0/7.0) * 7.0);
    return mix(colorG, colorA, (t - 6.0/7.0) * 7.0);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {    
    vec2 uv = fragCoord.xy / iResolution.xy;
    float distortion = sin(uv.x * 10.0 + iTime) * 0.1 * sin(uv.y * 10.0 + iTime * 2.0);
    fragColor = vec4(getColor(uv.x + distortion), 1.0);
}
```

# ②シンプルに色を配置する
ここからまずはシンプルにXY位置を指定するだけのコードを作りました。

https://www.shadertoy.com/view/XfyBW3

元々の`getColor`内で補間する方法だとx,y軸上での位置の固定を直感的にできないため、柔軟さに欠ける部分がありましたが、下記のように色に重みをつけるようにしたことで現在の位置`pos`から各色の中心までの距離が近いほどその色になるよう見た目を調整できました。

```glsl
float distA = length(pos - posA);
float distB = length(pos - posB);
...

float weightA = 1.0 / pow(distA, 3.0);
float weightB = 1.0 / pow(distB, 3.0);
...

float totalWeight = weightA + weightB + weightC + weightD + weightE + weightF + weightG;

vec3 col = (colorA * weightA + colorB * weightB + colorC * weightC +
            colorD * weightD + colorE * weightE + colorF * weightF +
            colorG * weightG) / totalWeight;
```

`weightA`でdistAを3乗していますが、そのままdistAで割ってしまうと色のピークとそれ以外の部分の差が大きくなってしまい見た目が微妙になるからです。
![スクリーンショット 2024-12-18 14.09.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/127b940e-4888-00db-f055-a119af123964.png)


# ③動きをつける
色がただ配置されているだけだと面白くないので動きをつけます。

https://www.shadertoy.com/view/4cGfW3

![capture (8).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/8cfed8b8-42b9-69f9-ffbe-d32077341fb5.gif)

画面の真ん中を中心にして回転するようになりました。

ひとつ前のコードからの変更点は下記です。
```glsl
// 回転角度を時間に基づいて計算
float angle = iTime * 0.5;
float cosA = cos(angle);
float sinA = sin(angle);

// 回転行列を適用
mat2 rotation = mat2(cosA, -sinA, sinA, cosA);

// 各色の座標を回転し、中心に近づける
vec2 posA = rotation * vec2(0.5, 0.0) ; // Red
vec2 posB = rotation * vec2(0.35, 0.35); // Orange
vec2 posC = rotation * vec2(0.0, 0.5); // Yellow
vec2 posD = rotation * vec2(-0.35, 0.35); // Green
vec2 posE = rotation * vec2(-0.5, 0.0); // Blue
vec2 posF = rotation * vec2(-0.35, -0.35); // Purple
vec2 posG = rotation * vec2(0.0, -0.5); // Pink
```

`mat2`（2x2の二次元行列）を利用して座標の中心をもとに回転させています。
詳しい仕組みに関してはこちらを見てみましょう。

https://thebookofshaders.com/08/?lan=jp

## さらに動きを複雑にする
ただ回転するだけだと面白みがないので各色が中心の座標に近づく動きも取り入れてみましょう。

https://www.shadertoy.com/view/XcGBD3

変更点はこちらです。
```glsl
// 時間に基づいて中心に近づく係数を計算
float centerFactor = 1.0 - 0.5 * sin(iTime * 0.5); // 0.5の範囲で中心に近づく
...

// 各色の座標を回転し、中心に近づける
vec2 posA = rotation * vec2(0.5, 0.0) * centerFactor; // Red
vec2 posB = rotation * vec2(0.35, 0.35) * centerFactor + 0.1; // Orange
vec2 posC = rotation * vec2(0.0, 0.5) * centerFactor - 0.1; // Yellow
vec2 posD = rotation * vec2(-0.35, 0.35) * centerFactor + 0.05; // Green
vec2 posE = rotation * vec2(-0.5, 0.0) * centerFactor - 0.05; // Blue
vec2 posF = rotation * vec2(-0.35, -0.35) * centerFactor + 0.2; // Purple
vec2 posG = rotation * vec2(0.0, -0.5) * centerFactor - 0.2; // Pink
```

中心に近づいたり離れたりする処理を追加しつつ、それぞれのタイミングを少しずつずらして複雑な挙動のように見せています。

# ④fbmで見た目をイメージに近づける
今の絵でも十分綺麗ですが、ボヤッとした色の塊が動いているだけなので見ている人にイメージが伝わりません。
絵の具がじわっと水中で広がっていくようなイメージにしたいのでfbmで自然な広がりを演出しましょう。

※fbmとそこで使うパーリンノイズに関しては今回は説明しません。分からない人は一旦なんか見た目をいい感じにできるやつとして覚えておいてください。

https://www.shadertoy.com/view/4fGBD3

![capture-_9_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/accd8b3c-5a33-f49d-5c00-2ad2a960e62a.gif)

```glsl
// FBMを使用して流体の動きをシミュレート
float noise = fbm(pos * 5.0 + iTime * 0.1);
pos += vec2(noise, noise) * 0.5; // ノイズの影響を調整
```

posにノイズを追加するような形です。

# ⑤ポストエフェクトをかける
いよいよ完成系のコードと同じ見た目になります。

https://www.shadertoy.com/view/lfKyRw

![capture-_11_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/4c700cac-e68a-164d-3d85-c9bc2b18f295.gif)

さらに液体っぽい見た目を作るために水滴が一定のタイミングで落ちて波紋が広がるようなエフェクトを入れましょう。
わかりやすく実装するためにパスを増やしてポストエフェクトとして水滴のエフェクトを追加します。

今までのコードを`bufferA`に移動し、`image`タブに新たに下記の記述を追加します。
```glsl
vec2 random2(vec2 p) {
    return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453);
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 pos = fragCoord.xy / iResolution.xy;
    
    float dropInterval = 3.0;
    float dropTime = mod(iTime, dropInterval);
    vec2 dropPos = random2(floor(vec2(iTime / dropInterval)));
    
    if (dropTime < 1.5) { 
        float dist = length(pos - dropPos);
        float ripple = sin(dist * 30.0 - dropTime * 5.0) * exp(-dist * 10.0);
        float fade = smoothstep(1.5, 0.0, dropTime); 
        pos += vec2(ripple, ripple) * 0.5 * fade;
    }
    
    vec4 color = texture(iChannel0, pos);
    fragColor = color;
}
```

`dist`で落とす位置とピクセル位置との距離を測り、時間で減衰するように処理して座標を変化させています。


iChannel0に`bufferA`のパスを通したら完成です！
![スクリーンショット 2024-12-18 14.43.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/bc33b0cf-acfe-e49b-c42a-28896ab7059d.png)

# 最後に
このコード自分でも結構気に入っています。
これを派生させて色々と表現の幅を広げていこうと思います。

読んでくださりありがとうございました。

# 参考にしたサイト

https://qiita.com/hiranuma/items/6e7cc424232d68e1e250

https://thebookofshaders.com/08/?lan=jp