【Unity】Shaderを用いて斜めHPバーを作ってみた

# はじめに
皆さんは、HPゲージの表示のさせ方について悩んだことがあるでしょうか？
Unityではゲージを表現するための機能がいくつか用意されていますが、元の機能のみではどうも思ったような表現が出来ないことがあります。
例えば、斜めのHPバーをImageのfillAmountで作ろうとすると、以下の画像のように現在値が増減させるときに縦に真っすぐな線になってしまいます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/4becde54-13d0-ec3e-7e78-13479e610d0f.png" width=300px height=30px>　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/2f79e27a-5ca6-0b70-eac4-685c5982687f.png" width=300px height=30px>
こうではなく、右の画像のように傾きを持たせたいですね。
今回は、このような斜めになった、少しスタイリッシュなHPゲージをShaderを用いて作成していきます。
# 環境

+ Unityバージョン　：Unity2020.3.18f1
+ テキストエディタ　：Microsoft Visual Studio 2019

# 目次
1. よく使われるゲージの増減手法
1. Shaderを使う準備をする
1. Shaderを書く
1. 中身を見る
1. C#スクリプトを書いて動かしてみる
1. おわりに

# 1. よく使われるゲージの増減手法
### 1-1. Sliderを使う
斜めのHPゲージを作る前に、他にどのような手法が使われているか簡単に見ていきましょう。

https://docs.unity3d.com/ja/2019.4/Manual/script-Slider.html

Slider（スライダー）はツマミを掴み、ドラッグすることでゲージの値を変化させることが出来るものです。よく**ゲームの音量調整や明るさ調整など**に使用されています。
このツマミを削除し、ゲージの変化をスクリプト側から行うことでHPゲージのようなふるまいをさせることも可能です。
エディタ上のタブの`GameObject/UI/Slider`から作成できます。
### 1-2. ImageのfillAmountを使う

https://docs.unity3d.com/ja/2018.4/ScriptReference/UI.Image-fillAmount.html

fillAmountは、様々な手法でImageの表示面積を削ることができる機能です。縦にゲージを減らしたり、円形に減らすなど、Sliderより様々な表現を作り出すことが出来ます。
まず、`GameObject/UI/Image`からImageを作成し、InspectorからImageコンポーネントのSource Imageに変化させるSpriteをアタッチします。その後、Image TypeからFilledを選択するとFill Amountのプロパティが表示されます。
![3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/97116d0e-3366-0f58-014e-e1864291c0f2.png)
実際にこのスライダーを動かしてみると、Imageが削られているのが確認できます。
Fill MethodやFill Originの値も変更して遊んでみましょう。

# 2. Shaderを使う準備をする
では、すぐに斜めに傾けるShaderを書く...といきたいところですが、いくつか準備をする必要があります。

+ Spriteを用意する
+ Shaderファイルを作成する
+ ShaderをアタッチするMaterialを作成する
+ MaterialをGameObjectにアタッチする

### 2-1. Spriteを用意する

<table>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/b7ee2a89-6d84-7041-dbc4-32be8937e8fd.png" alt="Rectangle"></td>
</table>

今回はこちらの白色の画像を使用します。
Unityを`3D`で起動し、この画像のTexture Typeを`Default → Sprite (2D and UI)`へ変更し、`Apply`を押して変更を確定します。
### 2-2. Shaderファイルを作成する
Projectウィンドウから、右クリックし、`Create/Shader/Image Effect Shader`を選択します。
### 2-3. ShaderをアタッチするMaterialを作成する
2-2と同様にして、`Create/Material`から新しいMaterialを作成します。
その後、先ほど作成したShaderファイルを**ドラッグ＆ドロップ**でこのMaterialにアタッチします。
![4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/106bc329-eacc-2836-3e66-ea1cae68167c.png)
これで、このMaterialに先ほど作成したShaderが適用されました。
画像のように、**Materialのプレビューが真っ黒になっていれば成功**です。
### 2-4. MaterialをGameObjectにアタッチする
最後にこのMaterialを適用するGameObjectを作成します。
Hierarchyウィンドウから、右クリックし、`UI/Image`から作成します。
すると、Canvasの中にImageコンポーネントを持つゲームオブジェクトが生成されます。

次に、Imageを選択し、先ほどの横長の画像を`Source Image`に、作成したマテリアルを`Material`にアタッチします。
以下のような画像になっていれば大丈夫です。
![5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/4fb5526b-2730-f81e-35f3-8b118dfea195.png)
最後に、一番下にある`Set Native Size`をクリックし、Imageのサイズを整えます。
これによって、Imageにも真っ黒なMaterialが適用されました。
# 3. Shaderを書く
では準備もできましたし、Shaderファイルを開いてみましょう。
いきなり大量にコードが出てきましたが、ご安心ください。今回はこのうちの一部を変更・追加するのみですので。
以下のように書き換えてみましょう。

```SlantHPShader.shader
Shader "Hidden/SlantHPShader"
{
    Properties // プロパティを置ける
    {
        // 追加
        _FillAmount("FillAmount", Range(0.0, 1.0)) = 1.0 // 値を増減させるためのプロパティ
        _Color ("Color", Color) = (1, 1, 1, 1) // 色を変更するためのプロパティ
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always
        Blend SrcAlpha OneMinusSrcAlpha // 追加

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            // 追加
            float4 _MainTex_TexelSize; // テクスチャのサイズに関する情報を持つ
            float _FillAmount;
            fixed4 _Color;

            fixed4 frag(v2f i) : SV_Target
            {
                // 追加
                fixed4 color = _Color; // 処理中の座標の色を_Colorに
                float h = _MainTex_TexelSize.w; // テクスチャの縦のピクセル数
                float w = _MainTex_TexelSize.z; // テクスチャの横のピクセル数

                // 左端を斜めにする処理
                if (i.uv.y > i.uv.x * w / h) {
                    color.a = 0.0; // 色のアルファ値を0(透明)にする
                }
                // 右端を斜めにする処理
                else if (i.uv.y < i.uv.x * w / h - (w - h) / h * _FillAmount) {
                    color.a = 0.0; // 色のアルファ値を0(透明)にする
                }

                return color;
            }
            ENDCG
        }
    }
}
```

1行目の名前は`"Hidden/(このシェーダーの名前)"`となるようにしましょう。それ以外の部分であればコピペしても正しく動作するかと思います。
# 4. 中身について
さてこのスクリプトの中ではどのような動作が行われているのでしょうか。変更した部分について、詳しく見ていきましょう。
### 4-1. Propertiesブロック内

https://docs.unity3d.com/ja/2019.4/Manual/SL-Properties.html

>ShaderLab: Properties
>シェーダーを使って Unity の マテリアルインスペクター でアーティストが設定するパラメーターを定義できます。

ここで宣言した変数が、**MaterialのInspector画面から調整できるようになります。**
作成したMaterialのInspectorを見てみると、下画像のように`FillAmount`と`Color`の2つの値が調整できるようになっていることがわかります。
![6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/cb2b25d9-8689-f8cc-8b16-d1a7c6921a06.png)
実際にこの値を動かしてみましょう。
FillAmountの値を動かすと、**0から1の範囲**で、それに合わせて用意していたImageのゲージが変化していきます。しっかり傾いた状態を維持していますね。
Colorは今回、Imageコンポーネントの色を取得するつもりなので、ここでは**白色のままにしておきます。**
### 4-2. fragブロック内
このブロック内では、Imageの両端が斜めになるような数学的な処理と色変更を行っています。
斜めの表現を作るために、描画する範囲を**領域の考え方**を用います。
![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/007409df-2305-5d86-c4d5-eec78cfd65f8.png)
上の画像のように、まず**両端の傾け度合い**を考えます。今回は一番単純な**45度**の傾きを考えました。
用意したSpriteの横の長さを`w`、縦の長さを`h`とすると、このような式を作ることができます。

次にこれを直接プログラムに書き記したいところですが、xとyを表現する際に使用する`i.uv`という変数は**それぞれ0~1の範囲しか持たない**ため、上の式を下図のように書き換える必要があります。
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/c70c0c90-d418-1148-57e2-1c1b62aede82.png)
xy方向の長さをそれぞれ1とする場合、それぞれの値に`w`や`h`の**逆数を掛ける**ことで割合を求めることが出来ます。
さらに、この右辺の切片の部分にPropertiesブロック内で宣言した`fillAmount`の値を掛けることで**ゲージの増減を考えることが出来ます。**つまり、このようになります。

```math
y > \frac{w}{h} x - \frac{w - h}{h} \times fillAmount
```

そして最後に、これをプログラムに落とし込みます。
今回は領域内を描画するのではなく、反対に**領域外を描画しない（不透明度を0にする）方法**を取ります。そのため、図に表示されている**不等号を反対向きにする**必要がありますね。

```hlsl
// 左端を斜めにする処理
if (i.uv.y > i.uv.x * w / h)
{
    color.a = 0.0; // 色のアルファ値を0(透明)にする
}
// 右端を斜めにする処理
else if (i.uv.y < i.uv.x * w / h - (w - h) / h * _FillAmount)
{
    color.a = 0.0; // 色のアルファ値を0(透明)にする
}
```

そして、この条件に一致する場合に`color.a = 0.0`と不透明度を0にすることで、範囲外を見せないようにしています。
# 5. C#スクリプトを書いて操作してみる
では最後に、このFillAmountの値をC#スクリプトから変更してみましょう。

```GaugeController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GaugeController : MonoBehaviour
{

    private Material mat; // Materialのインスタンス
    private Image img; // アタッチされたゲームオブジェクトのImageコンポーネント
    [SerializeField, Range(0.0f, 1.0f)] private float fillAmount = 1.0f; // ゲージ量

    // Start is called before the first frame update
    void Start()
    {
        img = GetComponent<Image>(); // Imageコンポーネント取得
        mat = Instantiate(img.material); // Materialのインスタンス生成
        mat.SetColor("_Color", img.color); // Materialの色指定
        img.material = mat; // ImageのMaterialをインスタンス化したMaterialに変更
    }

    // Update is called once per frame
    void Update()
    {
        mat.SetFloat("_FillAmount", fillAmount);
    }

    // ゲームオブジェクトが破棄されたときに呼び出される
    private void OnDestroy()
    {
        // インスタンスを持っていたら破棄する
        if (mat != null)
        {
            Destroy(mat);
        }
    }
}
```

### 5-1. Materialのインスタンスを作成する
現在ImageコンポーネントにアタッチしているMaterialは、Assetsフォルダ内のMaterialを**直接指定しています。**そのため、このまま`FillAmount`の値を変更しようとすると、**他のゲームオブジェクトまで値が変更されてしまいます。**
それを回避するために、スクリプト内でMaterialのインスタンスを生成し、アタッチしなおしています。
### 5-2. 色や値を変化させる
実際にこのスクリプトをゲームオブジェクトにアタッチしてみましょう。
色も実際のHPバーっぽく黄緑色にしてみました。
![10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/292dc139-85ec-c1b5-aa0c-54a36a8823a6.png)
そして実行し、スクリプトのFillAmountのスライダーを動かすと、実際に動くのが確認できます。
これで、斜めに傾いたHPバーが完成しました。
この値を別スクリプトから参照したり、色を変えたりと様々な場面で使うことが出来そうです。
# 6. おわりに
今回は、斜めに傾いたHPバーをShaderを用いて作成しました。
まさか、数学の領域の知識を使うことになるとは思わず、Unityで使う場面があったことに喜びを隠せませんでした。
ただ、Shaderの中にif文を書くことは、**パフォーマンスの低下に繋がる**ためあまり良くないという話をよく耳にします。そのため、これを如何にして改善するのか、今後の課題にしたいと思っています。

ありがとうございました！
# 参考文献

https://qiita.com/Teach/items/08701010e077b1933923

https://docs.unity3d.com/ja/2019.4/Manual/SL-PropertiesInPrograms.html