【Godot】放射ブラーの実装方法

# 概要
この記事では、Godot Engineで放射ブラーを実装する方法について書きます。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/b1da2d00-a658-ce57-95c5-7b365e7f9dfa.gif)
放射ブラーとは、このように指定した位置の中心から広がるようにブラーがかかる演出となります。

# 放射ブラーの実装
## 素材画像
放射ブラーの動作確認用の画像です。"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 int   samples  : hint_range(2, 64, 2) = 16;  // サンプル回数
uniform float strength : hint_range(0.0, 1.0) = 0.0; // ブラーの広がる強さ
uniform vec2  center = vec2(0.5, 0.5); // 中心座標

void fragment() {
	// 最終的な色
	vec4 color = vec4(0.0); // 黒で初期化
	// 中心を基準にする
	vec2 pos   = UV - center;
	// 中心からの距離
	float dist = length(pos);
	float factor = strength / float(samples) * dist;
	for(int i = 0; i < samples; i++) {
		float uvOffset = 1.0 - factor * float(i);
		color += texture(TEXTURE, pos * uvOffset + center);
	}
	// 平均を求める
	color /= float(samples);
	
	COLOR = color;
	
	// テスト用
	//COLOR.rgb = vec3(dist);
}
```

`CanvasItem > Shader > Shader Param > Strength` でブラーの強さ（大きさ）が指定できます。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/b1da2d00-a658-ce57-95c5-7b365e7f9dfa.gif)
放射ブラーのしくみとしては、 **元の画像を拡大しつつ何回もピクセルの書き込みを行う処理**となります。
「[【Godot】2Dモーションブラーの実装方法](https://qiita.com/2dgames_jp/items/856d1616642d698ee131)」 で紹介したモーションブラーが「ベクトル（移動量）」のブラーであるのに対して、放射ブラーはスケール（拡大縮小）のブラーと言えるのかもしれません。

なお、`Samples` パラメータがブラーのピクセル書き込み回数です。この値を大きくするほどブラーのぼかし品質が上がりますが処理負荷は増大します。だからといって回数を少なくすぎると境界が目立ちすぎるので、個人的には`Samples`は「8〜16回」くらいが良いのかなと思います。

## クリックした位置に放射ブラーをかける
スクリプトから、このブラー処理を呼び出せるようにします。
bgノード(`Sprite`)にスクリプトをアタッチして、以下のように記述します。

```gd:bg.gd
extends Sprite

const TIME_BLUR := 2.0; # 2秒

# ブラーの有効時間
var time_blur := 0.0

func start_radial_blur(pos:Vector2) -> void:
	# テクスチャのサイズ
	var tex_size := texture.get_size()
	
	# シェーダーに渡すブラーの中心座標
	var blur_center := Vector2()
	blur_center.x = pos.x / tex_size.x
	blur_center.y = pos.y / tex_size.y
	
	# 中心座標を設定
	material.set_shader_param("center", blur_center)
	
	# 放射ブラー開始
	time_blur = TIME_BLUR
	
func _input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		if event.is_pressed():
			# クリックで放射ブラー開始
			start_radial_blur(get_viewport().get_mouse_position())
	
func _process(delta: float) -> void:
	
	material.set_shader_param("strength", 0)
	if time_blur > 0:
		# シェーダー更新
		time_blur = max(0, time_blur - delta)
		# sinカーブで 0.0〜1.0 の半円を移動する
		var force := abs(sin(PI * time_blur / TIME_BLUR))
		material.set_shader_param("strength", force)
```

実行すると、クリックした位置を中心に放射ブラーがかかるようになります。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/b7129bf9-131f-a96d-5f5d-75a9e2be12fc.gif)

# 放射ブラーが適用されない部分を作る
最後に、放射ブラーが適用されない部分を設定できるようにします。
シェーダーコードを以下のように修正します。

```glsl
shader_type canvas_item;

uniform int   samples  : hint_range(2, 64, 2) = 16;  // サンプル回数
uniform float strength : hint_range(0.0, 1.0) = 0.0; // ブラーの広がる強さ
uniform float mask     : hint_range(0.0, 1.0) = 0.0; // ブラーが適用されないサイズ
uniform vec2  center = vec2(0.5, 0.5); // 中心座標

void fragment() {
	// 最終的な色
	vec4 color = vec4(0.0); // 黒で初期化
	// 中心を基準にする
	vec2 pos   = UV - center; 
	// 中心からの距離
	float dist = length(pos);
	float factor = strength / float(samples) * dist;

	// ブラーが適用されないされない範囲を計算。0.1の範囲をぼかす
	factor *= smoothstep(mask-0.1, mask, dist);
	
	for(int i = 0; i < samples; i++) {
		float uvOffset = 1.0 - factor * float(i);
		color += texture(TEXTURE, pos * uvOffset + center);
	}
	// 平均を求める
	color /= float(samples);
	
	COLOR = color;
	
	// テスト用
	//COLOR.rgb = vec3(dist);
}
```

`mask`というパラメータが新たに追加されています。
これは中心から放射ブラーを適用しないサイズとなります。
また、適用する部分と適用しない部分で不連続だと境界が目立ちすぎてしまうので、`smoothstep`で補間処理を入れてぼかすようにしています。

実行すると放射ブラーの除外範囲が指定できます。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/a429dc47-430c-3d21-c273-53bfbaa2b4b2.gif)


なぜこのような記述をするのかについては、以前に「[衝撃波シェーダー](https://qiita.com/2dgames_jp/items/d0e782911bf15dd58aee)」を作る際に紹介した動画が参考になると思います。

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

# 追記 (2021.2.23): 回転放射ブラーを実装する
シェーダーで遊んでいたら、回転放射ブラーが実装できたので追記します。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/46302a8d-a6c2-6d85-5f03-5847720e3728.gif)

```glsl
shader_type canvas_item;

uniform int   samples  : hint_range(2, 64, 2) = 16;  // サンプル回数
uniform float strength : hint_range(0.0, 1.0) = 0.0; // ブラーの広がる強さ
uniform float mask     : hint_range(0.0, 1.0) = 0.0; // ブラーが適用されないサイズ
uniform float vortex   : hint_range(0.0, 20) = 0.0;  // 回転値(ラジアン)
uniform vec2  center = vec2(0.5, 0.5); // 中心座標

void fragment() {
	// 最終的な色
	vec4 color = vec4(0.0); // 黒で初期化
	// 中心を基準にする
	vec2 pos   = UV - center; 
	// 中心からの距離
	float dist = length(pos);
	// 中心からの角度
	float angle = atan(pos.y, pos.x);
	float factor = strength / float(samples) * dist;

	// ブラーが適用されないされない範囲を計算。0.1の範囲をぼかす
	factor *= smoothstep(mask-0.1, mask, dist);
	
	for(int i = 0; i < samples; i++) {
		float uvOffset = 1.0 - factor * float(i);
		// 回転をかける
		vec2 ofs = vec2(dist * cos(angle + vortex * uvOffset), dist * sin(angle + vortex * uvOffset));
		color += texture(TEXTURE, ofs * uvOffset + center);
	}
	// 平均を求める
	color /= float(samples);
	
	COLOR = color;
	
	// テスト用
	//COLOR.rgb = vec3(dist);
}
```

`Vortex` で回転値を指定します。

# プロジェクトファイル
プロジェクトファイルは以下からダウンロードできます
http://syun777.sakura.ne.jp/tmp/godot/TestRadialBlur.zip

# 参考

* [【Unity】【シェーダ】Radial Blur（放射状ブラー）のポストエフェクトを実装する](https://light11.hatenadiary.com/entry/2019/01/04/232733)