03WeponEffectShader解説 UVスクロール＋マスキング


HAMELN 03 Wepon Package
https://nonamere.booth.pm/ #booth_pm 

上記の素材に同梱しているShaderについての解説記事です。
＊Unity向けのShaderです ParticleSystem用の最適化（CustomVertexStremsへの対応）はしていません

今回のShader適用例
<iframe width="560" height="315" src="https://www.youtube.com/embed/-f1qFV96Mxk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

---

## はじめに
この記事は03 Wepon Packageに内包するEffect用Shaderを改修しようとする際にShaderを触ったことが無い方向け用の解説記事です。
Shaderをある程度触ったことがある方([こちら](https://docs.unity3d.com/ja/2021.3/Manual/SL-VertexFragmentShaderExamples.html)が理解できている方)は
Shaderのフラグメントブロックの内容を見て頂けますと幸いです。

### 簡単な改変をしたい場合の対応方法
　![スクリーンショット 2022-11-18 165431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/3c7fee4e-1525-223c-ddf0-55a8f6d75b12.png)

発光色を変えたい
＝＞　マテリアルのColor欄を任意の色に変える

エフェクトの柄を変える
＝＞　マテリアルのMainTex欄に設定する画像を変える


---

## Shaderパラメータ説明
　
Shaderファイル配置場所
![スクリーンショット 2022-11-19 011202.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/d1bdff28-ef01-65ad-3670-99b3ed9d959e.png)

Shaderパス HAMELN/Effect/ActivateEffect
　![スクリーンショット 2022-11-18 165431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/3c7fee4e-1525-223c-ddf0-55a8f6d75b12.png)

- Color: Effectに設定するHDRカラー
[HDRとは](https://www.eizo.co.jp/eizolibrary/color_management/hdr/)
簡単にいうとデフォルト0~1の色の値以上の色を設定できるようにするもの(発光しているのと同義です)
　![スクリーンショット 2022-11-18 175822.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/d66a69a7-9b8f-7df5-bf7e-176ae0bc6b18.png)
RGBに対してIntensityで色の値をオフセットしています。

- MainTex: Effectに設定するテクスチャ
  - テクスチャのR値 UVスクロールする柄1
  
  - テクスチャのG値 UVスクロールする柄2
  
  - テクスチャのB値 エフェクトのアルファマスク

- Alpha: テクスチャのR値の柄を適用する強さ
　- 0:適用しない
　- 1:適用する

- ScrollX SCrollY
  - テクスチャのR値の柄のUVスクロールスピード
 
- Scroll2X SCroll2Y
  - テクスチャのG値の柄のUVスクロールスピード

---


## Shader全文
<details><summary>コード</summary>

``` hlsl
Shader "HAMELN/Effect/ActivateEffect" {
	Properties{
		[HDR]_Color("Color"       , Color) = (1, 1, 1, 1)
        //R: G: B:
		_MainTex ("MainTex", 2D) = "white" {}
		_Alpha ("Alpha", Range(0, 1)) = 0.5
		_Scroll_X ("ScrollX", Float) = 1
		_Scroll_Y ("ScrollY", Float) = 1
        _Scroll2_X ("Scroll2X", Float) = 1
        _Scroll2_Y ("Scroll2Y", Float) = 1
	}

	SubShader
	{
		Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
		//Blend SrcAlpha OneMinusSrcAlpha
        Blend SrcAlpha One
        LOD 200
        Cull Off
		ZWrite Off

		Pass
		{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Scroll_X;
            float _Scroll_Y;
            float _Alpha;
            float _Scroll2_X;
            float _Scroll2_Y;

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

            fixed4 frag(v2f i) : SV_Target
            {
                float2 uv2 = i.uv;
                float2 uv3 = i.uv;
                i.uv.x += _Time * _Scroll_X;
                i.uv.y += _Time * _Scroll_Y;
                uv2.x += -1 * _Time * _Scroll2_X;
                uv2.y += -1 * _Time * _Scroll2_Y;

                // sample the texture
                fixed Msk1 = tex2D(_MainTex, i.uv).r;
                fixed Msk2 = tex2D(_MainTex, uv2).g;
                fixed Msk3 = tex2D(_MainTex, uv3).b;

                half4 col1 = half4(1, 1, 1, Msk1);
                col1 *= _Color;
                col1.a *= _Alpha * pow(Msk3, 3);

                half4 col2 = half4(1, 1, 1, Msk2);
                col2 *= _Color;
                col2.a *= Msk3;
                col1 += pow(col2.a, 2);

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col1);
                return col1;
            }
            ENDCG
		}
	}
}
```
</details>

---

## Shader説明
![shaderinfo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/ae3a699c-e270-2290-2298-a172212c5544.png)

### ①プロパティブロック
 ここはUnityのInspectorで操作可能なマテリアルプロパティを表示させるための設定です。
 プロパティの書式は下記となります

>⑥で宣言した変数名("Inspectorに表示したい文字列", タイプ) = タイプ別の初期値

参考: https://docs.unity3d.com/ja/2021.3/Manual/SL-Properties.html

　![スクリーンショット 2022-11-18 165431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/3c7fee4e-1525-223c-ddf0-55a8f6d75b12.png)


### ②SubShaderブロック/GPU設定
　ここではShaderのGPU設定を行います
- [Tagsに関して](https://docs.unity3d.com/ja/2021.3/Manual/SL-SubShaderTags.html)
- [LODに関して](https://docs.unity3d.com/ja/2021.3/Manual/SL-ShaderLOD.html)
- [Blendに関して](https://docs.unity3d.com/ja/2018.4/Manual/SL-Blend.html)
  透明なテクスチャを合成できるようにしているのはここのBlend設定によるものです。
ここの設定次第では、透過合成の他にも乗算や加算のような合成表現が可能となります

| Blendを正しく設定している場合 | Blend設定を無効化(コメントアウト)した場合|
|:-:|:-:|
|![スクリーンショット 2022-11-18 173206.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/e6188595-1fa0-a0fc-04cb-8d0c35341adb.png)|![スクリーンショット 2022-11-18 173016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/ebbca706-e93c-f2f4-9de9-4530109fd015.png)|

- [CullとZWriteについて](https://docs.unity3d.com/ja/2018.4/Manual/SL-CullAndDepth.html)
Cull設定により、メッシュの裏面を描画するか（両面描画にするか）などの設定を行うことができます
ZWrite設定により、深度情報を書き込むかの設定を行うことができます

| Cull ZWriteをコメントアウト | Cull off ZWrite offで設定した場合 |
|:-:|:-:|
| ![スクリーンショット 2022-11-18 174446.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/86c5b1fa-ed29-0bef-4d5c-fc1a322a2b98.png)| ![スクリーンショット 2022-11-18 174427.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/74f1013f-df31-3d00-ba5f-fc06d4adb813.png)|
|cullがデフォルトonのためメッシュ裏面が描画されていない見た目となってしまっている|ZWrite offにすることで深度情報を書き込まず、半透明に半透明を重ねる場合の描画の不具合にある程度対処しています|


（3DCGの半透明描画の不具合は色々とあり、先人たちの奮闘の歴史が調べると出てくるので色々とみてみると面白いです

メッシュの裏面とは？
![スクリーンショット 2022-11-21 132111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/38021586-76ad-eebd-b56c-286c743639c4.png)
メッシュには、面ごとに法線という向きを持っています。（写真水色の線）
この反対方向が俗にいう裏面となります。
よくリッチなモデルで用いられる[NormalMap](https://docs.unity3d.com/ja/2021.3/Manual/StandardShaderMaterialParameterNormalMap.html)は、この面ごとの法線とNormalMapの持つ法線情報を掛け合わせることで
法線を変更しています。


### ③Passブロック
　ここでは一回の描画命令で処理する内容の設定を行います
　Shaderによってはこの描画命令が複数ある場合があります。(UTS2は４パス使ってたりします
　このPassブロック数(描画命令回数)が増えると描画コストが増えてしまい、ゲームが重たくなってしまいます。

> CGPROGRAM ~ ENDCG

これはここの中でプログラムを書くよ～みたいな相図みたいなものです。

> #pragma vertex vert

これは頂点シェーダとしてどの関数を呼び出すかの指定を行っています
今回は⑦の部分の関数を指しています。

#### 頂点シェーダとは
[頂点シェーダについて](https://learning.unity3d.jp/1622/)
ものすごく簡単に要約すると、メッシュの頂点毎にモデル空間の頂点座標(モデル原点から見た場合の頂点座標位置)から
カメラ空間の頂点座標(カメラから見た場合の頂点座標位置)への座標変換をしている部分です。

応用していくと頂点を動かすアニメーションみたいな仕組みも作れます
<iframe width="560" height="315" src="https://www.youtube.com/embed/qXcxBw3KUtw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


> #pragma fragment frag

これはフラグメントシェーダとしてどの関数を呼び出すかの指定を行っています
今回は⑧の部分の関数を指しています。

#### フラグメントシェーダとは
[フラグメンシェーダについて](https://docs.unity3d.com/ja/2019.4/Manual/SL-VertexFragmentShaderExamples.html)
ものすごく簡単にようやくすると頂点シェーダを元に、画面の１ピクセルに表示する色を決定するための部分です。

> #pragma multi_compile_fog

一旦おまじないとして認識してください
Unityのフォグ機能を適用するためのものです。

> #include "UnityCG.cginc"

Unityが提供してくれている便利な関数やマクロなどを使うための読み込み宣言です。

＊.cgincは自分でも作成することができます
Shaderを良く書いている中で似たような機能を何回も各場合は自作した.cgincファイルへ纏めるようにしましょう。

### ④頂点データの構造を宣言するブロック

メッシュの頂点には、モデルの原点から見た場合のローカル座標位置やＵＶ座標位置、頂点カラー、法線情報などを持っています

Blenderのメッシュタブの内容などを見てもらえるとわかりやすいかもしれません
![スクリーンショット 2022-11-18 231123.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/5b9975ec-460b-94aa-d756-b50a87fa8a81.png)

これらをShaderの中で取得するために頂点データがどのようなデータを持っているのかを宣言する必要があります。

宣言する場合は下記のようにパラメータを宣言する必要があります。
> 型 変数名 : 入力セマンティックス;

[セマンティックスとは](https://learn.microsoft.com/ja-jp/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics?redirectedfrom=MSDN)
要約すると、この変数にこの頂点データを流し込むよという宣言

### ⑤フラグメントデータの構造を宣言するブロック
頂点データから計算した内容を⑧のフラグメントシェーダで使うための構造を宣言する必要があります。

宣言する場合は下記のようにパラメータを宣言する必要があります。
＊④頂点データの構造を宣言するブロックの宣言と同形式。
> 型 変数名 : 入力セマンティックス;

### ⑥パラメータ宣言
⑦⑧などの関数内で使うための変数の宣言。
ここで宣言されている変数を①のプロパティブロックに記述することでUntiyのインスペクターから制御できるようになります。

宣言形式は下記
> 型 変数名;

今回のShaderでは

```hlsl
            //Effectのカラー
            fixed4 _Color;

            //MainTexのためのテクスチャ情報用変数
            sampler2D _MainTex;
            //MainTexのタイリングオフセット用変数
            float4 _MainTex_ST;

            //MainTex.R用のUVスクロールスピード制御用変数
            float _Scroll_X;
            float _Scroll_Y;
            //MainTex.Rの適用度制御用
            float _Alpha;

            //MainTex.G用のUVスクロールスピード制御用変数
            float _Scroll2_X;
            float _Scroll2_Y;
```

### ⑦頂点シェーダ

やっていることは⑧のフラグメントシェーダへ⑤の構造形式でデータを渡すための計算をしています。

```hlsl
            //⑤で宣言した構造体名 ③の#pragma vertex行で設定した名称 (④で宣言した構造体名 引数名)
            v2f vert(appdata v)
            {
                //⑧のフラグメントシェーダへ渡す構造体変数の宣言
                v2f o;
                
                //座標変換のための計算
                o.vertex = UnityObjectToClipPos(v.vertex);
                
                //_MainTex上のUV座標位置計算
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                //フォグの適用
                UNITY_TRANSFER_FOG(o, o.vertex);

                //⑧へ構造体変数を返却
                return o;
            }
```


### ⑧フラグメントシェーダ

やっていることは⑧の構造体変数を受け取り、画面の１ピクセルの色を決定します。
よく解像度が高いゲームの描画が重くなる理由は、１ピクセル毎に画面解像度分このフラグメントシェーダの処理が走るためです。


今回のShaderではＲ・Ｇ値の柄でそれぞれUＶスクロールしつつ合成、合成結果のαをＢ値でマスクする機能で構成されています。

```hlsl
            //不動点小数４配列型(R,G,B,A)　③の#pragma fragment行で設定した名称(⑤で宣言した構造体名 引数名) : System-Value 出力セマンティック 大概はSV_Targetでいいです
            fixed4 frag(v2f i) : SV_Target
            {
                //MainTex.G値UVスクロール用のUV宣言
                float2 uv2 = i.uv;
                //MainTex.B値UVスクロール用のUV宣言
                float2 uv3 = i.uv;

                //MainTex.R値UVスクロール用のUV座標ずらし計算
                i.uv.x += _Time * _Scroll_X;
                i.uv.y += _Time * _Scroll_Y;

                //MainTex.G値UVスクロール用のUV座標ずらし計算
                uv2.x += -1 * _Time * _Scroll2_X;
                uv2.y += -1 * _Time * _Scroll2_Y;

                // UV座標位置の色を取得する処理
                //i.uvの位置のMainTex色のR値を取得
                fixed Msk1 = tex2D(_MainTex, i.uv).r;
                //uv2の位置のMainTex色のG値を取得
                fixed Msk2 = tex2D(_MainTex, uv2).g;
                //uv3の位置のMainTex色のB値を取得
                fixed Msk3 = tex2D(_MainTex, uv3).b;

                //色を計算
                //考え方は_ColorのA値にMainTex.R値とMainTex.Ｇ値を掛け合わせ

                //MainTex.R値の色を計算
                half4 col1 = half4(1, 1, 1, Msk1);
                col1 *= _Color;
                //MainTex.B値の形でマスクしつつ、_Alpha(0~1)でMainTex.R値の適用度を決定
                col1.a *= _Alpha * pow(Msk3, 3);

                //MainTex.G値の色を計算
                half4 col2 = half4(1, 1, 1, Msk2);
                col2 *= _Color;
                //MainTex.B値の形でマスク
                col2.a *= Msk3;

                //col1とcol2を合成
                col1 += pow(col2.a, 2);

                // Untiyのフォグ機能を適用
                UNITY_APPLY_FOG(i.fogCoord, col1);

                //ピクセルの最終的なカラーを出力
                return col1;
            }
```

#### UVスクロール


> _Time

これはUnity側が提供してくれているゲーム内経過時間を持っている変数です。

[UnityBuildIn変数について](https://docs.unity3d.com/ja/2018.4/Manual/SL-UnityShaderVariables.html)

これをUV座標に合成します

> i.uv.x += _Time * _Scroll_X;
>i.uv.y += _Time * _Scroll_Y;

元のUVの座標位置に経過時間分の増加量を合成しUV座標位置が移動していくことをUVスクロールといいます。


Blender画面左のUV座標がいどうしている形になります。
![スクリーンショット 2022-11-18 183210.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/9e2c4c5e-2387-7cae-d97e-d909a1a75e99.png)


*ちなみに、Unity側のテクスチャのインポート設定内WrapModeがRepeatになっていないと正しい見た目になりません
<iframe width="560" height="315" src="https://www.youtube.com/embed/-qPoGprmBnc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

![スクリーンショット 2022-11-19 004707.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/03e45d9a-52a9-dbba-b94d-1f414149536a.png)




WarpModeがClampの場合、UVスクロールを行うとテクスチャ端でスクロールが止まってしまい、引き伸ばされたような見た目になります

<iframe width="560" height="315" src="https://www.youtube.com/embed/HEZXByTxDHU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


WarpModeがMirrorの場合、UVスクロールを行うとテクスチャ端でUVが反転し繰り返されるような見た目になります。
<iframe width="560" height="315" src="https://www.youtube.com/embed/tVZxMTSBhds" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

このようなUVスクロールをする場合に用いるTextureはテクスチャを繰り返しても問題ないような見た目になっている（する必要があるともいう）のですが、
そういうテクスチャをシームレステクスチャ　意味としては(つなぎ目がないテクスチャ)　と呼ばれています。


#### マスク処理について

> col1.a *= _Alpha * pow(Msk3, 3);
> col2.a *= Msk3;

![スクリーンショット 2022-11-19 010238.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/2e19c977-c00a-fd79-4c98-d2cce71ea318.png)


今回のシェーダでは上記の通りMainTex.B値の形でマスクしています。
この処理はMainTex.B値の値で最終的な見た目を切り抜いている(マスキングしている)と同義です。

マスク処理をコメントアウト(無効化)した場合

![スクリーンショット 2022-11-19 005458.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/0506f89b-f8e7-8c60-8c98-6e9addd9eb64.png)

マスクを反転させたい場合の記述例

> col1.a *= _Alpha * pow(1 - Msk3, 3);
> col2.a *= 1 - Msk3;

![スクリーンショット 2022-11-19 005832.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201345/e1e0265c-ac25-794e-754f-c901b1318a99.png)

このような、1-元の値 みたいな反転処理は良くOneMinus(ワンマイナス)と呼ばれます。
[BlendMode](https://docs.unity3d.com/ja/2019.4/Manual/SL-Blend.html)の設定パラメータ名にOneMinusみたいな名称が入っているのはこういう処理が入ってます。


### まとめ

今回のShaderは２つのUVスクロールを行い、それを合成、合成結果をマスクする
という処理をしています。

もし追加でテクスチャを合成したいや、RG毎に色を変えたいみたいな改修をしたい場合、
①と⑥に必要なパラメータを追加、そのパラメータを用いて⑧で計算処理を追加みたいな流れになります。


以上、Shaderの全体的な解説と今回のShaderの機能内容に関する説明記事でした。
ここが良く分からないみたいな箇所があればぜひご質問ください！

よきShaderライフを