【Godot】衝撃波シェーダーの作り方

# 概要
この記事では、Godot Engineでの衝撃波シェーダーの作り方を紹介します。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/13e6e1cf-033d-f8e0-acf7-5b264b16849b.gif)

といっても、以下の動画の内容で実装できるので、シェーダーコードはほぼそのままです

* [GODOT TUTORIAL : Shockwave shader for noobs](https://www.youtube.com/watch?v=SCHdglr35pk)

より詳しい解説を聞きたい場合には動画が参考になると思います（とてもわかりやすい説明です）

# 衝撃波シェーダーの実装
## 素材画像
衝撃波の動作確認用の画像です。"bg.png" として保存します。
![bg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/ccf10616-a80e-f10b-9a45-c370ee7f582e.png)

## 画像の登録
画像をプロジェクトに追加して、ドラッグ＆ドロップで配置します。
![Godot_Engine_-_TestShockWave____.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/d81c2946-c542-bd14-1a9c-4030563b5a47.png)

## シェーダーの作成
bgノード (`Sprite`) のインスペクタから、`CanvasItem > Material > Material > [空]` をクリックして、「新規 Shader Material」を選びます。
![Godot_Engine_-_TestShockWave_-_Main_tscn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/ed11c496-75fb-1cff-d30e-ca249a936998.png)
作成した Shader Materialをクリックして、`Shader > [空]` をクリック、「新規 Shader」を選びます。
![Godot_Engine_-_TestShockWave_-_Main_tscn____.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/279ce0b1-b3de-d25f-7182-27e970520f04.png)
作成した Shaderをクリックすると、シェーダーエディタが表示されます。
![Godot_Engine_-_TestShockWave_-_Main_tscn____.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/2bc6e018-d91d-0820-e63d-4b1d16c7fee8.png)

## シェーダーコードの記述
シェーダーコードは以下のように記述します。

```glsl
shader_type canvas_item;

uniform vec2 center = vec2(0.5, 0.5); // 衝撃波の中心
uniform float force     : hint_range(-1.0, 1.0) = 0.1; // 歪みの強さ
uniform float size      : hint_range( 0.0, 2.0) = 0.0; // 円の半径
uniform float thickness : hint_range(0.01, 1.0) = 0.1; // ドーナツの厚み

void fragment() {
	// テクスチャ比を計算
	float ratio = TEXTURE_PIXEL_SIZE.x / TEXTURE_PIXEL_SIZE.y;
	
	// X方向の比率を変換する (x=0.0〜1.0 をテクスチャの比率にあわせる)
	vec2 center2 = vec2(0.5 + (center.x-0.5) / ratio, center.y);
	
	// テクスチャ比率に依存しない真円にする
	vec2 scaleUV = (UV - vec2(0.5, 0.0) ) / vec2(ratio, 1.0) + vec2(0.5, 0.0);

	// エルミート補間でドーナツ型のマスクサイズを決める
	float mask = (1.0 - smoothstep(size-0.1, size, length(scaleUV - center2))) *
			smoothstep(size-thickness-0.1, size-thickness, length(scaleUV - center2));
	
	// 中心からの歪みを計算	
	vec2 disp = normalize(scaleUV - center2) * force * mask;
	COLOR = texture(TEXTURE, UV - disp);
	
	// 確認用.
	//COLOR.rgb = vec3(mask);
}
```

現在のテキスチャに適用するために元のコードを修正しています。
（おそらく運用を考えると Viewport Texture に適用していくのがいいのでは……と考えこのようにしました）

もし動画のように画面全体に適用する場合は、以下の修正をします。

* `TEXTURE_PIXEL_SIZE` を `SCREEN_PIXEL_SIZE` に修正
* `UV` を `SCREEN_UV` に修正
* 中心座標 `center` のX方向の比率をあわせる処理は不要

テクスチャの比率ではなく画面比率（アスペクト比）で計算して、スクリーンバッファのUVを使うことになります。

また、中心座標 `center` の比率を左側 0.0 右側 1.0 に合わせたかったので、
![Godot_Engine_-_TestShockWave_-_Main_tscn____.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/99ddbc8b-f175-9846-ebf6-04a27efe7597.png)

```glsl
	// X方向の比率を変換する (x=0.0〜1.0 をテクスチャの比率にあわせる)
	vec2 center2 = vec2(0.5 + (center.x-0.5) / ratio, center.y);
```

という逆変換の処理を入れていますが、これもおそらく不要となります。


## 動作確認
`CanvasItem > Shader Param` からパラメータの変化に対する動作を確認します。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/60b0a081-9620-b672-95cf-097249c5cf91.gif)

画面中心から衝撃波が広がっていくのが確認できます。

## クリックした位置から衝撃波が出現するようにする
最後に、スクリプトからこのシェーダーを呼び出すスクリプトを書きます。
bgノードに以下のスクリプトをアタッチします。

```gd:bg.gd
extends Sprite

# 衝撃波が有効かどうか
var shockwave_enabled := false

# 衝撃波の中心座標
var shockwave_center := Vector2()

# 衝撃波の半径
var shockwave_radius := 0.0

# 衝撃波の太さ
var shockwave_thickness := 0.1

# 衝撃波の強さ
var shockwave_force := 0.1

func start_shockwave(pos:Vector2):
	# 衝撃波演出開始
	shockwave_enabled = true
	shockwave_radius = 0
	shockwave_thickness = 0.1
	shockwave_force = 0.2
	
	# テクスチャサイズで割合を求める
	var tex_size := texture.get_size()
	shockwave_center.x = pos.x / tex_size.x
	shockwave_center.y = pos.y / tex_size.y

func _input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		if event.is_pressed():
			# クリックで衝撃波演出開始
			start_shockwave(get_viewport().get_mouse_position())

func _process(delta: float) -> void:
	if shockwave_enabled:
		# 半径サイズを更新
		shockwave_radius += delta
		shockwave_force *= 0.93 # 徐々に弱くする
		
		# 各種シェーダーパラメータを設定
		material.set_shader_param("center",    shockwave_center)
		material.set_shader_param("force",     shockwave_force)
		material.set_shader_param("size",      shockwave_radius)
		material.set_shader_param("thickness", shockwave_thickness)
		
		if shockwave_force < 0.001:
			# 衝撃波終了
			material.set_shader_param("force", 0.0)
			shockwave_enabled = false	
```

実行してクリックした位置に衝撃波が出ることを確認します。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/13e6e1cf-033d-f8e0-acf7-5b264b16849b.gif)

# プロジェクトファイル
確認用にプロジェクトファイルをアップロードしておきました

* http://syun777.sakura.ne.jp/tmp/godot/TestShockWave.zip

# 参考

* [GODOT TUTORIAL : Shockwave shader for noobs](https://www.youtube.com/watch?v=SCHdglr35pk)