GLSLで始めるShaderコーディング入門

今回は次のキラキラシェーダーの作り方について解説します。  
GLSLコーティングの初心者の方もイメージを掴めるように心がけます。
なお、サンプルはWebでGLSLを実行できるエディタ[** The Book of Shaders Editor **](http://editor.thebookofshaders.com/)で確認しました。

<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215221257.gif" title="キラキラシェーダー"> 


開発アプリPR
<a href="https://play.google.com/store/apps/details?id=jp.gr.java.conf.createapps.musicline">
    <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231111/20231111170552.png" alt="３分作曲-musicLine-" title="3分作曲-musicLine- Androidアプリ">
</a>

<br>
<br>

# キラキラシェーダーの構成要素

キラキラシェーダーは`パターン`、`カラー`、`アルファ`の3種類の要素があります。タイル状に十字を敷き詰めたパターンをアニメーションするカラー・アルファでブレンドすることで作っています。

|構成要素|イメージ|
|:---:|:---:|
|パターン|<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215232931.png" title="パターン"> |
|カラー|<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215231654.gif" title="カラー"> |
|アルファ|<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215220218.gif" title="アルファ"> |

<br>

## パターンの作成

### ST座標

<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215213424.png" title="ST座標">

```glsl
precision mediump float;
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy; // 正規化
    gl_FragColor = vec4(st, 0., 1.); 
}
```  

`gl_FragCoord`はピクセル座標で左下原点`（0, 0）`で右上が画面サイズ（例えば`(300, 300)`）となります。
`st`は`gl_FragCoord`を画面サイズ`u_resolution`で割ることで、左下原点`（0, 0）`で右上`(1, 1)`となるST座標へ変換します。  

このサンプルでは座標を可視化するため、`gl_FragColor`の赤と緑の値にXとYの値を渡して色を表示しています。なので、値が大きくなる程色がつきます。
例えば、座標`（1, 1）`はRGB`（1, 1, 0）`となり黄色になります。

### タイル座標

<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215213140.png" title="タイル座標"> 

```glsl
...
float repeat_count = 30.; // 繰り返し回数
float aspect = .8; // 縦横比
vec2 scaled_st = repeat_count * vec2(1., aspect) * st; // スケーリング 
vec2 difference_st = scaled_st + vec2(0, .3) * mod(floor(scaled_st).x, 5. ); // ずらす
vec2 tile =  fract(difference_st); // 0 ~ 1 の間を繰り返す
gl_FragColor = vec4(tile, 0., 1.); 
```

パターンは同じ形が繰り返し描画されるため、ST座標をタイル状に分割します。この例ではY軸方向に少しずらしてタイルの単調さを軽減しています。

`scaled_st`で繰り返す回数分座標空間を大きくします。なお十字は縦長のため、アスペクト比をかけています。
`difference_st`ではX座標に応じて、Y軸方向へ座標をずらしています。`mod`は余剰のため、この例では`0 ~ 4`を繰り返します。
`fract`は小数点のみ取得するので、`0 ~ 1`を繰り返すタイルパターンになります。

### 十字パターン

<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215232931.png" title="十字パターン"> 

```glsl
vec2 tile_center = abs(tile - .5); // 端 0.5 中心 0.
float circle = step(.6, 1. - length(tile_center)); // 円
float grid = min(1., step(.42, .5 - tile_center.x) + step(.42, .5 - tile_center.y / aspect)); // グリッド
float cross = circle * grid;
gl_FragColor = vec4(vec3(cross), 1.);
```

`tile_center`はタイルの中心からの距離で、タイル中心が`（0, 0）`になるように`-0.5`しています。
`step`は閾値で 0 か 1 に変換します。`circle`はタイル中心からの距離を閾値とし円に型抜きします。`grid`はX軸とY軸の各々で距離を閾値として格子状に型抜きします。`circle`と`grid`のANDを取ることで十字ができます。

### （菱形パターン）

<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215232929.png" title="菱形パターン"> 

```glsl
vec2 tile_center = abs(tile - .5);    // 端 0.5 中心 0.
float tile_dist = 1. - step(.3, tile_center.x + tile_center.y);
float diamond = step(1., tile_dist);
gl_FragColor = vec4(vec3(diamond), 1.);
```

ここの処理を変更することで色々な図形パターンを作ることができます。

<br>

## カラーの作成

<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215231654.gif" title="カラー"> 

```glsl
vec2 tile_pos = floor(difference_st);
vec3 shift = vec3(200.0 * (random(tile_pos * 2.) + u_time * 0.2), 1.0, 1.0);
vec3 random_col = shift_col(vec3(1.0, 0.3, 0.3), shift);
gl_FragColor = vec4(random_col, 1.);
```

`tile_pos`は`floor`でST座標を整数化することでタイル位置を算出しています。`tile_pos`をシード値として乱数を取得することで、タイル毎に色をランダムに決定します。また`u_time`で時間により色が変化します。  
`random`は乱数取得、`shift_col`は色変更の自作関数です。

<br>

```glsl
highp float random(highp vec2 st){
	return fract(sin(dot(st.xy ,vec2(12.9898,78.233))) * 43758.5453);
}

vec3 shift_col(vec3 RGB, vec3 shift) {
    vec3 RESULT = vec3(RGB);
    highp float VSU = shift.z * shift.y * cos(shift.x * 3.14159265 / 180.0);
    highp float VSW = shift.z * shift.y * sin(shift.x * 3.14159265 / 180.0);
    
    RESULT.x = (.299 * shift.z + .701 * VSU + .168 * VSW) * RGB.x
    	+ (.587 * shift.z - .587 * VSU + .330 * VSW) * RGB.y
    	+ (.114 * shift.z - .114 * VSU - .497 * VSW) * RGB.z;
    
    RESULT.y = (.299 * shift.z - .299 * VSU - .328 * VSW) * RGB.x
    	+ (.587 * shift.z + .413 * VSU + .035 * VSW) * RGB.y
    	+ (.114 * shift.z - .114 * VSU + .292 * VSW) * RGB.z;
    
    RESULT.z = (.299 * shift.z - .3 * VSU + 1.25 * VSW) * RGB.x
    	+ (.587 * shift.z - .588 * VSU - 1.05 * VSW) * RGB.y
    	+ (.114 * shift.z + .886 * VSU - .203 * VSW) * RGB.z;
    
    return (RESULT);
}
```
GLSLには乱数を取得する標準関数がないので`random`関数を自作します。Shaderでの乱数の取得は一般的に計算のオーバーフローを使ったやり方で取得するみたいです。  
`shift_col`は第一引数に`色`、第二引数に`シフト値`を指定します。シフト値は（色相、彩度、明度）となっており、色相は360で一周し、彩度と明度は倍率となっています。

<br>

## アルファの作成

<img width="300" src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231215/20231215220218.gif" title="アルファ"> 

```glsl
float cycle_time = 5.; // 光る周期時間
float on_rat = .6; // 全体に対して光る割合
float PI = 3.14159265359;
highp float animation_offset = random(tile_pos); // アニメーションの開始オフセット      
float alpha = sin((animation(on_rat, cycle_time, u_time, animation_offset) - 0.25) * PI * 2.) * .5 + .5;
gl_FragColor = vec4(vec3(alpha), 1.);
```

`sin`を使うことでイーズイン・アウトのアニメーション効果をつけてループ再生します。`animation_offset`を乱数にすることで、タイル毎にバラバラに光るようにしています。  
`animation`は自作の関数です。

```glsl
 highp float animation(highp float animation_ratio, highp float cycle_time, highp float time, highp float offset){
     highp float start_time = offset; // 0. ~ 1.
     highp float end_time = start_time + animation_ratio;
     highp float pos_in_cycle = mod(time, cycle_time) / cycle_time; // 0. -> 1.
     highp float pos1 = smoothstep(start_time, end_time, pos_in_cycle);
     highp float pos2 = smoothstep(start_time, end_time, pos_in_cycle + 1.); // 境界用
     return max(fract(pos1), fract(pos2));
 }
```

アニメーションの開始時間`start_time`と終了時間`end_time`は0~1+(a)に正規化しています。
時間`time`と光る周期時間`cycle_time`により、`pos_in_cycle`サイクル中の進行位置を出しています。
アニメーション開始時間がサイクル終了よりの場合、`end_time`が1以上になりアニメーションがサイクルを跨ぐ場合があります。そのパターンを考慮して、アニメーション進行位置を`pos1`と`pos2`の2つを使います。



<br>

以上、今回のサンプルで取り上げたキラキラシェーダーの解説でした。
元記事ではAndroidアプリでの活用例を紹介しています！
また、ソースコード等の詳細も元記事を参照ください。

https://musicline-developer.hatenablog.com/entry/2023/12/16/012630

<br>

開発アプリPR
<a href="https://play.google.com/store/apps/details?id=jp.gr.java.conf.createapps.musicline">
    <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/musicline_developer/20231111/20231111170552.png" alt="３分作曲-musicLine-" title="3分作曲-musicLine- Androidアプリ">
</a>

<br>