🤔

[Godot Engine]シェーダーでゼルダの伝説BotW「がんばりの器」風ゲージを作る

2020/10/26に公開


©Nintendo

ゼルダの伝説 ブレスオブザワイルド の「がんばりの器」を表現するゲージを作成します。
ゲーム内では、リンクを強化していくことで外枠が付いてゲージが大きくなっていくのですが、今回作成するものは簡易的なもので、単純な円形ゲージになります。

サンプルではスクリプトでuniformで渡す値を調整したり、Tweenしたりしてますが、ほとんどの処理はシェーダーでやっています。

シェーダー

Canvas Itemを継承するノードでシェーダーマテリアルを設定し、シェーダーを割り当てます。
そして以下のシェーダーを書いてみました。

shader_type canvas_item;

const float pi = 3.14159;
const float circle = 4.0;

uniform vec4 color:hint_color = vec4(1.0);
uniform vec4 target_color:hint_color = vec4(1.0);
uniform float size:hint_range(0.0, 1.0) = 1.0;
uniform float inner_ratio:hint_range(1.0, 10.0) = 10.0;
uniform float amount:hint_range(0, 1.0) = 1.0;
uniform float target:hint_range(0, 1.0) = 1.0;

void fragment()
{
	//UVの中心座標
	vec2 uv = UV - 0.5;
    
	//UVを極座標に変換
	float angle = atan(uv.y, uv.x);
	//右から開始になるので反時計回りに90度回転
	angle += (pi / 2.0);
    
	//360度に変換
	float a = fract(0.5 * angle / pi);
    
	//ターゲット量に応じてアルファ値を切り捨て
	float ta = 1.0 - step(target, a);
	//現在の量に応じてアルファ値を切り捨て
	float ca = 1.0 - step(amount, a);
	
	// uniformで受け取った指定色で描画
	COLOR.rgb = vec3(target_color.rgb) * max(0, ca - ta);
	COLOR.rgb += vec3(color.rgb) * (ca - max(0, ca - ta));
	COLOR.a = min(1.0 ,ca + 0.2);
	
	// uniformで受け取ったサイズの円形クリップ用マスク
	float mask = 1.0 - smoothstep(size - (size * 0.01), size, dot(uv, uv) * circle);
	// 内側の円を抜いてマスクに加算
	mask *= smoothstep(size - (size * 0.01), size, dot(uv, uv) * (circle * inner_ratio));
	
	// 最終的なクリップ
	COLOR.a *= mask;
}

解説

前半部分

//UVの中心座標
vec2 uv = UV - 0.5;

//UVを極座標に変換
float angle = atan(uv.y, uv.x);
//右から開始になるので反時計回りに90度回転
angle += (pi / 2.0);

//360度に変換
float a = fract(0.5 * angle / pi);

uv変数は中心座標にしているだけです。
大文字のUVはGodot Engineの組み込み変数で、解像度からUVを割り出す処理を行った後の値です。(便利)
なので、-0.5することで基点が縦横軸ともに半分ズレます。

angle変数では、uvの値をatanで角度を求めています。
この時点でCOLORで出力するとわかるのですが、右側から180度の扇型になります。
上側から始めたいので、angle変数にPI/2.0を足して90度回転させます。

最後に、fract関数で360度の円形を作ってa変数に渡しました。

中盤部分

//ターゲット量に応じてアルファ値を切り捨て
float ta = 1.0 - step(target, a);
//現在の量に応じてアルファ値を切り捨て
float ca = 1.0 - step(amount, a);

// uniformで受け取った指定色で描画
COLOR.rgb = vec3(target_color.rgb) * max(0, ca - ta);
COLOR.rgb += vec3(color.rgb) * (ca - max(0, ca - ta));
COLOR.a = min(1.0 ,ca + 0.2);

前半で作ったa変数をCOLORで出力するとわかるのですが、グラデーションを極座標的に歪めた絵が出来上がっています。

これができたら後は単純です。
step()関数は、しきい値よりも大きいか小さいかで、01を返す関数です。

ta変数・ca変数に、現在の量に応じて上図のグラデーションのどまでこを切り捨てて、どこまでを切り上げるかを01.0の間で指定しstep()関数で計算します。
これで割合でゲージを表示させることができます。


amountを0.7程度にして出力した結果

上図は0.7以上の値であれば0にして、0.7未満であれば1にするという計算がされた結果を表示しました。
ca現在の量を表し、ta現在の量からの差分を表示します。

// uniformで受け取った指定色で描画
COLOR.rgb = vec3(target_color.rgb) * max(0, ca - ta);
COLOR.rgb += vec3(color.rgb) * (ca - max(0, ca - ta));
COLOR.a = min(1.0 ,ca + 0.2);

COLORで少し汚い感じになってますが、以下のような感じになっています。

  1. 差分の量target_colorで指定した赤色で表示
  2. 現在の量 - 差分の量colorで指定した緑で表示
  3. その他の箇所は色を表示してないので黒になってる
  4. アルファ値は現在の量ca)に+0.2して、小さい方の値を使用
    • これによって、現在の量が表示されている箇所はアルファ値が1.0になり、表示されてない箇所は0 + 0.2 = 0.2で表示される。

後半部分

さて、ここまで出来たら後は円形にマスクしていきます。

// uniformで受け取ったサイズの円形クリップ用マスク
float mask = 1.0 - smoothstep(size - (size * 0.01), size, dot(uv, uv) * circle);
// 内側の円を抜いてマスクに加算
mask *= smoothstep(size - (size * 0.01), size, dot(uv, uv) * (circle * inner_ratio));

// 最終的なクリップ
COLOR.a *= mask;

smoothstep()関数で円を描いています。
GLSLではすごく基本的なところなので説明は割愛しますが、2つの円を合わせて、ドーナツ型にしています。

COLORのアルファ値に、抜きになる部分は0になるように乗算します。

スクリプトからマテリアルの設定を変更する

var _material:ShaderMaterial = $Gauge.get_material()
_material.set_shader_param("target", 0.9)

上記のように、ノードからget_material()で取得したマテリアルに対して、シェーダーパラメーター(uniform)を設定できるので、ヒットポイントやがんばり度を割合で渡すことで、動的にゲージを変化させることが出来ます。

Tweenを使う場合は文字列でプロパティにアクセスするので以下のように記述します。

$Tween.interpolate_property(_material, "shader_param/size", 
		0, 0.7, 2, Tween.TRANS_ELASTIC, Tween.EASE_OUT)

さいごに

Godot Engineのシェーダーは、ビューポート上でリアルタイムに表示されるので、かなり作りやすいです。
今回のようなゲージをスクリプトで作ると冗長なコードと複雑なアセットになりそうですが、シェーダーで作ることでだいぶシンプルに作れました。

「シェーダーはともだち!こわくないよ!」(キャ○翼)

Discussion